shithub: pplay

Download patch

ref: faa5d55642c8bbd26151561eef13b6525f786154
parent: 8d34476e2e8852e577d67f74ada5bf65c5701935
author: qwx <qwx@sciops.net>
date: Thu Dec 29 07:16:52 EST 2022

decouple memory buffers from chunks referencing them, no more copying

merry fucking christmas
- files are still pre-split when reading, but buffers are thenceforth untouched
- no more merging chunks or buffers
- generalize chunks as chains without a sentinel or dummy pointer
- separate chunk handling from editing (perhaps too much) to streamline and enforce
some rules
- avoid bugprone totalsz usage and other fixed references; recalculating
size after every edit isn't expensive in practice, the number of splits wouldn't
be in the order of thousands even
- fix race condition in rthread

needs more testing after recent changes
pending: undo/redo, registers, addressable multisnarf buffers

--- /dev/null
+++ b/chunk.c
@@ -1,0 +1,424 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include "dat.h"
+#include "fns.h"
+
+/* chunk ≡ chain of chunks */
+struct Buf{
+	uchar *buf;
+	usize bufsz;
+	Ref;
+};
+static Chunk *norris;
+
+static void
+printchunks(Chunk *r)
+{
+	Chunk *c;
+
+	fprint(2, "chunklist dot %zux %zux %zux: ", 
+		dot.from.pos, dot.pos, dot.to.pos);
+	c = r;
+	do{
+		fprint(2, "[%#p:%zd:←%#p→%#p] ", c, c->len, c->left, c->right);
+		assert(c->right->left == c);
+		c = c->right;
+	}while(c != r);
+	fprint(2, "\n");
+}
+
+static Chunk *
+newchunk(Buf *b)
+{
+	Chunk *c;
+
+	c = emalloc(sizeof *c);
+	c->left = c;
+	c->right = c;
+	c->b = b;
+	c->off = 0;
+	c->len = b->bufsz;
+	incref(&b->Ref);
+	return c;
+}
+static Chunk *
+newbuf(usize n)
+{
+	Buf *b;
+
+	assert((n & 3) == 0);
+	b = emalloc(sizeof *b);
+	b->bufsz = n;
+	b->buf = emalloc(b->bufsz);
+	return newchunk(b);
+}
+
+static void
+linkchunk(Chunk *left, Chunk *c)
+{
+	c->left->right = left->right;
+	left->right->left = c->left;
+	c->left = left;
+	left->right = c;
+}
+static void
+unlink(Chunk *left, Chunk *right)
+{
+	left->left->right = right->right;
+	right->right->left = left->left;
+	left->left = right;
+	right->right = left;
+}
+
+static Chunk *
+clonechunk(Chunk *c)
+{
+	Chunk *nc;
+
+	assert(c != nil && c->b != nil);
+	nc = newchunk(c->b);
+	nc->off = c->off;
+	nc->len = c->len;
+	incref(c->b);
+	return nc;
+}
+Chunk *
+replicate(Chunk *left, Chunk *right)
+{
+	Chunk *cl, *c, *nc;
+
+	cl = clonechunk(left);
+	c = cl;
+	for(; left!=right;){
+		left = left->right;
+		nc = clonechunk(left);
+		linkchunk(c, nc);
+		c = nc;
+	}
+	return cl;
+}
+
+static void
+freebuf(Buf *b)
+{
+	if(b == nil)
+		return;
+	free(b->buf);
+	free(b);
+}
+static void
+freechunk(Chunk *c)
+{
+	if(c == nil)
+		return;
+	if(c->b != nil && decref(c->b) == 0)
+		freebuf(c->b);
+	free(c);
+}
+void
+freechain(Chunk *c)
+{
+	Chunk *cl, *cp;
+
+	if(c == nil)
+		return;
+	for(cl=c->right; cl!=c; cl=cp){
+		cp = cl->right;
+		unlink(cl, cl);
+		freechunk(cl);
+	}
+	freechunk(c);
+}
+
+static void
+shrinkbuf(Chunk *c, usize newsz)
+{
+	Buf *b;
+
+	b = c->b;
+	assert(b != nil);
+	assert(newsz < b->bufsz && newsz > 0);
+	if(c->off + c->len > newsz)
+		c->len = newsz - c->off;
+	b->buf = erealloc(b->buf, newsz, b->bufsz);
+}
+
+#ifdef nope
+static Chunk *
+merge(Chunk *left, Chunk *right)
+{
+	uchar *u;
+	Buf *l, *r;
+	Chunk *nc;
+
+	if(left == right)
+		return left;
+	l = left->b;
+	r = right->b;
+	assert(l != nil && r != nil);
+	nc = newbuf(left->len + right->len);
+	u = nc->b->buf;
+	memcpy(u, l->buf+left->off, left->len);
+	memcpy(u + left->len, r->buf+right->off, right->len);
+	linkchunk(left->left, nc);
+	unlink(left, right);
+	freechain(left);
+	return nc;
+}
+
+Chunk *
+mergedot(usize *off)
+{
+	Chunk *left, *right;
+
+	left = p2c(dot.from.pos, off);
+	right = p2c(dot.to.pos, nil);
+	if(left == right)
+		return left;
+	while(left->right != right)
+		left = merge(left, left->right);
+	return merge(left, right);
+}
+#endif
+
+Chunk *
+p2c(usize p, usize *off)
+{
+	int x;
+	Chunk *c;
+
+	for(c=norris, x=0; p>=c->len; c=c->right){
+		if(c == norris && ++x > 1){
+			c = norris->left;
+			break;
+		}
+		p -= c->len;
+	}
+	if(off != nil)
+		*off = p;
+	return c;
+}
+usize
+c2p(Chunk *tc)
+{
+	Chunk *c;
+	usize p;
+
+	for(p=0, c=norris; c!=tc; c=c->right)
+		p += c->len;
+	return p;
+}
+
+void
+recalcsize(void)
+{
+	int n;
+
+	n = c2p(norris->left) + norris->left->len;
+	if(dot.to.pos == totalsz || dot.to.pos > n)
+		dot.to.pos = n;
+	if(dot.pos < dot.from.pos || dot.pos > dot.to.pos)
+		dot.pos = dot.from.pos;
+	totalsz = n;
+}
+
+#define ASSERT(x) {if(!(x)) printchunks(norris); assert((x)); }
+void
+paranoia(int exact)
+{
+	usize n;
+	Chunk *c, *pc;
+	Buf *b;
+
+	ASSERT(dot.pos >= dot.from.pos && dot.pos < dot.to.pos);
+	for(pc=norris, n=pc->len, c=pc->right; c!=norris; pc=c, c=c->right){
+		b = c->b;
+		ASSERT(b != nil);
+		ASSERT((b->bufsz & 3) == 0 && b->bufsz >= Sampsz);
+		ASSERT(c->off < b->bufsz);
+		ASSERT(c->len > Sampsz);
+		ASSERT(c->off + c->len <= b->bufsz);
+		ASSERT(c->left == pc);
+		n += c->len;
+	}
+	if(exact){
+		ASSERT(n <= totalsz);
+		ASSERT(dot.to.pos <= totalsz);
+	}
+}
+#undef ASSERT
+
+void
+setdot(Dot *dot, Chunk *right)
+{
+	dot->from.pos = 0;
+	if(right == nil)
+		dot->to.pos = c2p(norris->left) + norris->left->len;
+	else
+		dot->to.pos = c2p(right);
+}
+
+Chunk *
+splitchunk(Chunk *c, usize off)
+{
+	Chunk *nc;
+
+	if(off == 0)
+		return c;
+	assert(off <= c->len);
+	nc = clonechunk(c);
+	nc->off = c->off + off;
+	nc->len = c->len - off;
+	c->len = off;
+	linkchunk(c, nc);
+	return nc;
+}
+
+/* c1 [nc … c2] nc */
+int
+splitrange(usize from, usize to, Chunk **left, Chunk **right)
+{
+	usize off;
+	Chunk *c;
+
+	c = p2c(from, &off);
+	if(off > 0){
+		splitchunk(c, off);
+		*left = c->right;
+	}else
+		*left = c;	/* dangerous in combination with *right */
+	c = p2c(to, &off);
+	if(off < c->len - 1){
+		splitchunk(c, off);
+		*right = c;
+	}else
+		*right = c;
+	return 0;
+}
+
+Chunk *
+cutrange(usize from, usize to, Chunk **latch)
+{
+	Chunk *c, *left, *right;
+
+	if(splitrange(from, to, &left, &right) < 0)
+		return nil;
+	c = left->left;
+	if(left == norris)
+		norris = c;
+	unlink(left, right);
+	if(latch != nil)
+		*latch = left;
+	return c;
+}
+
+Chunk *
+croprange(usize from, usize to, Chunk **latch)
+{
+	Chunk *left, *right;
+
+	if(splitrange(from, to, &left, &right) < 0)
+		return nil;
+	norris = left;
+	*latch = right->right;
+	unlink(right->right, left->left);
+	return left;
+}
+
+// FIXME: generalized insert(from, to), where from and to not necessarily distinct
+Chunk *
+inserton(usize from, usize to, Chunk *c, Chunk **latch)
+{
+	Chunk *left;
+
+	left = cutrange(from, to, latch);
+	linkchunk(left, c);
+	return left;
+}
+
+Chunk *
+insertat(usize pos, Chunk *c)
+{
+	usize off;
+	Chunk *left;
+
+	if(pos == 0){
+		left = norris->left;
+		norris = c;
+	}else{
+		left = p2c(pos, &off);
+		splitchunk(left, off);
+	}
+	linkchunk(left, c);
+	return left;
+}
+
+uchar *
+getslice(Dot *d, usize n, usize *sz)
+{
+	usize Δbuf, Δloop, off;
+	Chunk *c;
+
+	if(d->pos >= totalsz){
+		werrstr("out of bounds");
+		*sz = 0;
+		return nil;
+	}
+	c = p2c(d->pos, &off);
+	Δloop = d->to.pos - d->pos;
+	Δbuf = c->len - off;
+	if(n < Δloop && n < Δbuf){
+		*sz = n;
+		d->pos += n;
+	}else if(Δloop <= Δbuf){
+		*sz = Δloop;
+		d->pos = d->from.pos;
+	}else{
+		*sz = Δbuf;
+		d->pos += Δbuf;
+	}
+	return c->b->buf + c->off + off;
+}
+
+Chunk *
+readintochunks(int fd)
+{
+	int n;
+	usize m;
+	Chunk *rc, *c, *nc;
+
+	for(m=0, rc=c=nil;; m+=n){
+		nc = newbuf(Iochunksz);
+		if(rc == nil)
+			rc = nc;
+		else
+			linkchunk(c, nc);
+		c = nc;
+		if((n = readn(fd, c->b->buf, Iochunksz)) < Iochunksz)
+			break;
+		yield();
+	}
+	close(fd);
+	if(n < 0)
+		fprint(2, "readintochunks: %r\n");
+	else if(n == 0){
+		if(c != rc)
+			unlink(c, c);
+		freechunk(c);
+		if(c == rc){
+			werrstr("readintochunks: nothing read");
+			return nil;
+		}
+	}else if(n > 0 && n < Iochunksz)
+		shrinkbuf(c, n);
+	return rc;
+}
+
+void
+graphfrom(Chunk *c)
+{
+	norris = c;
+	recalcsize();
+	setdot(&dot, nil);
+}
--- a/cmd.c
+++ b/cmd.c
@@ -6,154 +6,17 @@
 
 Dot dot;
 usize totalsz;
-static Chunk norris = {.left = &norris, .right = &norris};
-static Chunk *held;
-static uchar plentyofroom[Iochunksz];
-static int cutheld;
-static int epfd[2];
+int treadsoftly;
 
-static void
-printchunks(Chunk *r)
-{
-	Chunk *c;
+// FIXME: undo/redo as an unbatched series of inserts and deletes
+// FIXME: crazy idea, multisnarf with addressable elements; $n registers; fork pplay to display them → ?
 
-	fprint(2, "chunklist dot %zux %zux %zux: ", 
-		dot.from.pos, dot.pos, dot.to.pos);
-	c = r;
-	do{
-		fprint(2, "%#p:%zux:←%#p→%#p - ", c, c->bufsz, c->left, c->right);
-		assert(c->right->left == c);
-		c = c->right;
-	}while(c != r);
-	fprint(2, "\n");
-}
+enum{
+	Nhold = 64,
+};
+static Chunk *hold[Nhold], *snarf;
+static int epfd[2];
 
-static void
-recalcsize(void)
-{
-	int n;
-	Chunk *c;
-
-	for(c=norris.right, n=0; c!=&norris; c=c->right)
-		n += c->bufsz;
-	if(dot.to.pos == totalsz || n < totalsz && dot.to.pos > n)
-		dot.to.pos = n;
-	totalsz = n;
-}
-
-#define ASSERT(x) {if(!(x)) printchunks(&norris); assert((x)); }
-static void
-paranoia(int exact)
-{
-	usize n;
-	Chunk *c;
-
-	ASSERT(dot.pos >= dot.from.pos && dot.pos < dot.to.pos);
-	for(c=norris.right, n=0; c!=&norris; c=c->right){
-		ASSERT(c->buf != nil);
-		ASSERT((c->bufsz & 3) == 0 && c->bufsz >= Sampsz);
-		n += c->bufsz;
-	}
-	if(exact){
-		ASSERT(n <= totalsz);
-		ASSERT(dot.to.pos <= totalsz);
-	}
-}
-
-static Chunk *
-newchunk(usize n)
-{
-	Chunk *c;
-
-	assert((n & 3) == 0);
-	c = emalloc(sizeof *c);
-	c->bufsz = n;
-	c->buf = emalloc(c->bufsz);
-	c->left = c;
-	c->right = c;
-	return c;
-}
-
-static Chunk *
-clonechunk(void)
-{
-	Chunk *c;
-
-	assert(held != nil);
-	c = newchunk(held->bufsz);
-	memcpy(c->buf, held->buf, c->bufsz);
-	return c;
-}
-
-static void
-freechunk(Chunk *c)
-{
-	if(c == nil)
-		return;
-	free(c->buf);
-	free(c);
-}
-
-static void
-linkchunk(Chunk *left, Chunk *c)
-{
-	c->left->right = left->right;
-	left->right->left = c->left;
-	c->left = left;
-	left->right = c;
-}
-
-static void
-unlinkchunk(Chunk *c)
-{
-	c->left->right = c->right;
-	c->right->left = c->left;
-	c->left = c->right = nil;
-}
-
-static void
-resizechunk(Chunk *c, usize newsz)
-{
-	vlong Δ;
-
-	Δ = newsz - c->bufsz;
-	c->buf = erealloc(c->buf, newsz, c->bufsz);
-	c->bufsz = newsz;
-	if(c->right == &norris && Δ < 0)
-		dot.to.pos += Δ;
-}
-
-Chunk *
-p2c(usize p, usize *off)
-{
-	Chunk *c;
-
-	assert(p < totalsz);
-	c = norris.right;
-	while(p >= c->bufsz){
-		p -= c->bufsz;
-		c = c->right;
-	}
-	if(off != nil)
-		*off = p;
-	assert(c != &norris);
-	return c;
-}
-static usize
-c2p(Chunk *tc)
-{
-	Chunk *c;
-	usize p;
-
-	p = 0;
-	c = norris.right;
-	while(c != tc){
-		p += c->bufsz;
-		c = c->right;
-	}
-	return p;
-}
-
 void
 setrange(usize from, usize to)
 {
@@ -187,319 +50,113 @@
 }
 
 static int
-holdchunk(Chunk *c, int cut)
+replace(char *, Chunk *c)
 {
-	if(held != nil){
-		if(held == c)
-			return 0;
-		else if(cutheld)
-			freechunk(held);
-	}
-	held = c;
-	cutheld = cut;
-	if(cut){
-		unlinkchunk(c);
-		setpos(dot.from.pos);
-	}
-	return 0;
-}
+	Chunk *left, *latch;
 
-static Chunk *
-merge(Chunk *left, Chunk *right)
-{
-	usize Δ;
-
-	assert(right != &norris);
-	if(left->buf == nil || right->buf == nil){
-		werrstr("can\'t merge self into void");
-		return nil;
+	if(c == nil){
+		fprint(2, "replace: nothing to paste\n");
+		return -1;
 	}
-	if(left->buf != right->buf){
-		Δ = left->bufsz;
-		resizechunk(left, left->bufsz + right->bufsz);
-		memmove(left->buf + Δ, right->buf, right->bufsz);
-	}else{
-		right->buf = nil;
-		left->bufsz += right->bufsz;
+	if((left = inserton(dot.from.pos, dot.to.pos, c, &latch)) == nil){
+		fprint(2, "insert: %r\n");
+		return -1;
 	}
-	unlinkchunk(right);
-	freechunk(right);
-	return 0;
+	setdot(&dot, nil);
+	dot.pos = c2p(left->right);
+	return 1;
 }
 
-static Chunk *
-splitright(Chunk *left, usize off)
+static int
+insert(char *, Chunk *c)
 {
-	usize Δ;
-	Chunk *c;
+	Chunk *left;
 
-	Δ = left->bufsz - off;
-	if(off == 0 || Δ == 0)
-		return left;
-	c = newchunk(Δ);
-	memcpy(c->buf, left->buf+off, Δ);
-	resizechunk(left, off);
-	linkchunk(left, c);
-	return c;
-}
-
-static Chunk *
-mergedot(usize *off)
-{
-	usize p;
-	Chunk *c;
-
-	c = p2c(dot.from.pos, &p);
-	*off = p;
-	p = dot.from.pos - p;
-	while(p + c->bufsz < dot.to.pos)
-		merge(c, c->right);
-	return c;
-}
-
-/* before one may split oneself, one must first merge oneself */
-static Chunk *
-splitdot(void)
-{
-	usize p;
-	Chunk *c;
-
-	c = mergedot(&p);
-	splitright(c, p + dot.to.pos - dot.from.pos);
-	return splitright(c, p);
-}
-
-uchar *
-getslice(Dot *d, usize n, usize *sz)
-{
-	usize Δbuf, Δloop, off;
-	Chunk *c;
-
-	if(d->pos >= totalsz){
-		werrstr("out of bounds");
-		*sz = 0;
-		return nil;
+	if(c == nil){
+		fprint(2, "insert: nothing to paste\n");
+		return -1;
 	}
-	c = p2c(d->pos, &off);
-	Δloop = d->to.pos - d->pos;
-	Δbuf = c->bufsz - off;
-	if(n < Δloop && n < Δbuf){
-		*sz = n;
-		d->pos += n;
-	}else if(Δloop <= Δbuf){
-		*sz = Δloop;
-		d->pos = d->from.pos;
-	}else{
-		*sz = Δbuf;
-		d->pos += Δbuf;
+	if((left = insertat(dot.pos, c)) == nil){
+		fprint(2, "insert: %r\n");
+		return -1;
 	}
-	return c->buf + off;
+	setdot(&dot, nil);
+	dot.pos = c2p(left->right);
+	return 1;
 }
 
-vlong
-getbuf(Dot d, usize n, uchar *buf, usize bufsz)
-{
-	uchar *p, *b;
-	usize sz;
-
-	assert(d.pos < totalsz);
-	assert(n <= bufsz);
-	b = buf;
-	while(n > 0){
-		if((p = getslice(&d, n, &sz)) == nil || sz < Sampsz)
-			return -1;
-		memcpy(b, p, sz);
-		b += sz;
-		n -= sz;
-	}
-	return b - buf;
-}
-
-int
-advance(Dot *d, usize n)
-{
-	usize m, sz;
-
-	m = 0;
-	while(n > 0){
-		if(getslice(d, n, &sz) == nil)
-			return -1;
-		m += sz;
-		n -= sz;
-	}
-	return m;
-}
-
 static int
-insert(char *, Chunk *c)
+paste(char *s, Chunk *c)
 {
-	usize p;
-	Chunk *left;
-
-	if(c == nil && (c = clonechunk()) == nil){
-		werrstr("insert: no buffer");
+	if(c == nil && (c = snarf) == nil){
+		werrstr("paste: no buffer");
 		return -1;
 	}
-	left = p2c(dot.pos, &p);
-	splitright(left, p);
-	linkchunk(left, c);
-	setrange(dot.pos, dot.pos + c->bufsz);
-	return 1;
+	c = replicate(c, c->left);
+	if(dot.from.pos == 0 && dot.to.pos == totalsz)
+		return insert(s, c);
+	else
+		return replace(s, c);
 }
 
 static int
 copy(char *)
 {
-	Chunk *c;
+	Chunk *c, *left, *right;
 
-	c = splitdot();
-	holdchunk(c, 0);
+	splitrange(dot.from.pos, dot.to.pos, &left, &right);
+	c = replicate(left, right);
+	freechain(snarf);
+	snarf = c;
 	return 0;
 }
 
-static int
+static vlong
 cut(char *)
 {
-	Chunk *c;
+	Chunk *latch;
 
 	if(dot.from.pos == 0 && dot.to.pos == totalsz){
 		werrstr("cut: no range selected");
 		return -1;
 	}
-	c = splitdot();
-	holdchunk(c, 1);
+	cutrange(dot.from.pos, dot.to.pos, &latch);
+	setdot(&dot, nil);
 	return 1;
 }
 
 static int
-replace(char *, Chunk *c)
-{
-	Chunk *left, *right;
-
-	if(c == nil && (c = clonechunk()) == nil){
-		werrstr("replace: no buffer");
-		return -1;
-	}
-	right = splitdot();
-	left = right->left;
-	unlinkchunk(right);
-	freechunk(right);
-	right = left->right;
-	linkchunk(left, c);
-	setrange(dot.from.pos, right != &norris ? c2p(right) : totalsz);
-	return 1;
-}
-
-static int
-paste(char *s, Chunk *c)
-{
-	if(dot.from.pos == 0 && dot.to.pos == totalsz)
-		return insert(s, c);
-	return replace(s, c);
-}
-
-static int
 crop(char *)
 {
-	usize Δ;
-	Chunk *c, *d;
+	Chunk *latch;
 
-	Δ = 0;
-	for(c=norris.right; c!=&norris; c=d){
-		if(Δ + c->bufsz >= dot.from.pos)
-			break;
-		d = c->right;
-		Δ += c->bufsz;
-		unlinkchunk(c);
-		freechunk(c);
-	}
-	dot.from.pos -= Δ;
-	dot.to.pos -= Δ;
-	if(dot.from.pos > 0){
-		Δ = c->bufsz - dot.from.pos;
-		memmove(c->buf, c->buf + dot.from.pos, Δ);
-		resizechunk(c, Δ);
-		dot.to.pos -= dot.from.pos;
-		dot.from.pos = 0;
-	}
-	for(; c!=&norris; dot.to.pos-=c->bufsz, c=c->right)
-		if(c->bufsz >= dot.to.pos)
-			break;
-	if(dot.to.pos > 0)
-		resizechunk(c, dot.to.pos);
-	for(c=c->right; c!=&norris; c=d){
-		d = c->right;
-		unlinkchunk(c);
-		freechunk(c);
-	}
+	if(croprange(dot.from.pos, dot.to.pos, &latch) == nil)
+		return -1;
+	setdot(&dot, nil);
 	dot.pos = 0;
-	dot.to.pos = totalsz;
 	return 1;
 }
 
-static int
-forcemerge(char *)
+vlong
+getbuf(Dot d, usize n, uchar *buf, usize bufsz)
 {
-	usize p;
+	uchar *p, *b;
+	usize sz;
 
-	if(dot.from.pos == 0 && dot.to.pos == totalsz){
-		werrstr("merge: won\'t implicitely merge entire buffer\n");
-		return -1;
+	assert(d.pos < totalsz);
+	assert(n <= bufsz);
+	b = buf;
+	while(n > 0){
+		if((p = getslice(&d, n, &sz)) == nil || sz < Sampsz)
+			return -1;
+		memcpy(b, p, sz);
+		b += sz;
+		n -= sz;
 	}
-	mergedot(&p);
-	return 0;
+	return b - buf;
 }
 
-static Chunk *
-readintochunks(int fd)
-{
-	int n;
-	usize m;
-	Chunk *rc, *c, *nc;
-
-	for(m=0, rc=c=nil;; m+=n){
-		nc = newchunk(Iochunksz);
-		if(rc == nil)
-			rc = nc;
-		else
-			linkchunk(c, nc);
-		c = nc;
-		if((n = readn(fd, c->buf, Iochunksz)) < Iochunksz)
-			break;
-		yield();
-	}
-	close(fd);
-	if(n < 0)
-		fprint(2, "readintochunks: %r\n");
-	else if(n == 0){
-		if(c != rc)
-			unlinkchunk(c);
-		freechunk(c);
-		if(c == rc){
-			werrstr("readintochunks: nothing read");
-			return nil;
-		}
-	}else if(n > 0 && n < Iochunksz)
-		resizechunk(c, n);
-	return rc;
-}
-
 static int
-readfrom(char *s)
-{
-	int fd;
-	Chunk *c;
-
-	if((fd = open(s, OREAD)) < 0)
-		return -1;
-	c = readintochunks(fd);
-	close(fd);
-	if(c == nil)
-		return -1;
-	return paste(nil, c);
-}
-
-static int
 writebuf(int fd)
 {
 	static uchar *buf;
@@ -534,6 +191,21 @@
 	return 0;
 }
 
+int
+advance(Dot *d, usize n)
+{
+	usize m, sz;
+
+	m = 0;
+	while(n > 0){
+		if(getslice(d, n, &sz) == nil)
+			return -1;
+		m += sz;
+		n -= sz;
+	}
+	return m;
+}
+
 static void
 rc(void *s)
 {
@@ -555,19 +227,28 @@
 	close(fd);
 	threadexits(nil);
 }
+/* using a thread does slow down reads a bit */
+// FIXME: ugly
 static void
 rthread(void *efd)
 {
 	int fd;
+	Dot d;
 	Chunk *c;
 
+	d = dot;
+	treadsoftly = 1;
 	fd = (intptr)efd;
 	if((c = readintochunks(fd)) == nil)
 		threadexits("failed reading from pipe: %r");
 	close(fd);
+	dot = d;
 	paste(nil, c);
+	dot.pos = dot.from.pos;
+	setdot(&dot, nil);
 	recalcsize();
 	redraw(0);
+	treadsoftly = 0;
 	threadexits(nil);
 }
 
@@ -609,6 +290,20 @@
 	return pipeline(arg, 1, 1);
 }
 
+static int
+readfrom(char *s)
+{
+	int fd;
+
+	if((fd = open(s, OREAD)) < 0)
+		return -1;
+	if(threadcreate(rthread, (int*)fd, mainstacksize) < 0){
+		fprint(2, "threadcreate: %r\n");
+		return -1;
+	}
+	return 0;
+}
+
 /* the entire string is treated as the filename, ie.
  * spaces and any other weird characters will be part
  * of it */
@@ -657,7 +352,7 @@
 	case '|': x = pipeto(s); break;
 	case 'c': x = copy(s); break;
 	case 'd': x = cut(s); break;
-	case 'm': x = forcemerge(s); break;
+//	case 'm': x = forcemerge(s); break;
 	case 'p': x = paste(s, nil); break;
 	case 'q': threadexitsall(nil);
 	case 'r': x = readfrom(s); break;
@@ -678,9 +373,7 @@
 
 	if((c = readintochunks(fd)) == nil)
 		sysfatal("loadin: %r");
-	linkchunk(&norris, c);
-	recalcsize();
-	setrange(0, totalsz);
+	graphfrom(c);
 	return 0;
 }
 
--- a/dat.h
+++ b/dat.h
@@ -1,6 +1,7 @@
 typedef struct Chunk Chunk;
 typedef struct Pos Pos;
 typedef struct Dot Dot;
+typedef struct Buf Buf;
 
 enum{
 	Rate = 44100,
@@ -10,9 +11,11 @@
 	Outsz = WriteDelay * Sampsz,
 	Iochunksz = 4*1024*1024,	/* ≈ 24 sec. at 44.1 kHz */
 };
+#pragma incomplete Buf
 struct Chunk{
-	uchar *buf;
-	usize bufsz;
+	Buf *b;
+	usize off;
+	usize len;
 	Chunk *left;
 	Chunk *right;
 };
@@ -26,9 +29,11 @@
 };
 extern Dot dot;
 extern usize totalsz;
+extern int treadsoftly;
 
 extern int stereo;
 extern int debug;
+extern int debugdraw;
 
 #define MIN(x,y)	((x) < (y) ? (x) : (y))
 #define MAX(x,y)	((x) > (y) ? (x) : (y))
--- a/draw.c
+++ b/draw.c
@@ -6,6 +6,7 @@
 #include "fns.h"
 
 QLock lsync;
+int debugdraw;
 
 enum{
 	Cbg,
@@ -46,7 +47,7 @@
 
 	c = p2c(views, &off);
 	r = view->r;
-	for(p=views-off; p<viewe; p+=c->bufsz, c=c->right){
+	for(p=views-off; p<viewe; p+=c->len, c=c->right){
 		if(p == 0)
 			continue;
 		x = (p - views) / T;
@@ -56,7 +57,7 @@
 		r.min.x += x;
 		r.max.x = r.min.x + 1;
 		draw(view, r, col[Cchunk], nil, ZP);
-		if(c->bufsz == 0)
+		if(c->len == 0)
 			break;
 	}
 }
@@ -195,7 +196,7 @@
 		r.max.x = r.min.x + 1;
 		draw(view, r, col[Cloop], nil, ZP);
 	}
-	if(debug)
+	if(debugdraw)
 		drawchunks();
 }
 
--- a/fns.h
+++ b/fns.h
@@ -1,3 +1,15 @@
+void	freechain(Chunk*);
+void	recalcsize(void);
+void	paranoia(int);
+void	setdot(Dot*, Chunk*);
+Chunk*	replicate(Chunk*, Chunk*);
+int	splitrange(usize, usize, Chunk**, Chunk**);
+void	graphfrom(Chunk*);
+Chunk*	inserton(usize, usize, Chunk*, Chunk**);
+Chunk*	insertat(usize, Chunk*);
+Chunk*	croprange(usize, usize, Chunk**);
+Chunk*	cutrange(usize, usize, Chunk**);
+Chunk*	readintochunks(int);
 int	cmd(char*);
 void	initcmd(void);
 void	update(void);
@@ -12,6 +24,7 @@
 int	advance(Dot*, usize);
 int	jump(usize);
 Chunk*	p2c(usize, usize*);
+usize	c2p(Chunk*);
 void	setrange(usize, usize);
 int	setpos(usize);
 uchar*	getslice(Dot*, usize, usize*);
--- a/mkfile
+++ b/mkfile
@@ -3,6 +3,7 @@
 MAN=/sys/man/1
 TARG=pplay
 OFILES=\
+	chunk.$O\
 	cmd.$O\
 	draw.$O\
 	pplay.$O\
--- a/pplay.c
+++ b/pplay.c
@@ -91,7 +91,7 @@
 	Rune r;
 
 	ARGBEGIN{
-	case 'D': debug = 1; break;
+	case 'D': debug = 1; debugdraw = 1; break;
 	case 'c': cat = 1; break;
 	case 's': stereo = 1; break;
 	default: usage();
@@ -141,6 +141,7 @@
 			switch(r){
 			case Kdel:
 			case 'q': threadexitsall(nil);
+			case 'D': debugdraw ^= 1; break;
 			case ' ': toggleplay(); break;
 			case 'b': setjump(dot.from.pos); break;
 			case Kesc: setrange(0, totalsz); update(); break;
@@ -151,6 +152,10 @@
 			case '_': setzoom(-1, 1); break;
 			case '+': setzoom(1, 1); break;
 			default:
+				if(treadsoftly){
+					fprint(2, "dropping edit event during ongoing read\n");
+					break;
+				}
 				if((p = prompt(r)) == nil || strlen(p) == 0)
 					break;
 				qlock(&lsync);