    public static class Pipe extends java.io.Reader {
        private boolean eof = false;

        private char writeBuf = 0;
        
        private int numReadsSinceLastWrite = -1;
        
        public void write(char ch) {
            write(ch, false);
        }
        
        //NB: combine write() with eof() because the last write() will block, making it hard to call eof()
        public void writeFinal(char ch) {
            write(ch, true);
        }
        
        synchronized private void write(char ch, boolean isFinal) {
            //AC:System.err.println("write()");
            if(eof) {
                throw new IllegalStateException("Cannot write after EOF.");
            }
            if(isFinal) {
                eof = true;
            }
            writeBuf = ch;
            //AC:System.err.println("W: writing char '" + writeBuf + "'");
            //AC:System.err.println("W: # reads = 0");
            numReadsSinceLastWrite = 0;
            while(numReadsSinceLastWrite < 2) {
                try {
                    //AC:System.err.println("W: waiting");
                    notifyAll();
                    wait();
                    //AC:System.err.println("W: waking (# reads = " + numReadsSinceLastWrite + ")");
                } catch (InterruptedException e) {
                }
            }
            //AC:System.err.println("W: done");
            notifyAll();
        }
        
        @Override
        //NB: ignore length and always write 1 byte (unless at EOF)
        synchronized public int read(char[] buf, int offset, int length) throws java.io.IOException {
            //AC:System.err.println("read()");
            //nothing has been written yet - wait
            while(numReadsSinceLastWrite < 0) {
                if(eof) {
                    //AC:System.err.println("R: eof in first loop");
                    notifyAll();
                    return -1;
                }
                try {
                    //AC:System.err.println("R: waiting (first loop)");
                    notifyAll();
                    wait();
                    //AC:System.err.println("R: waking (first loop) (# reads = " + numReadsSinceLastWrite + ")");
                } catch (InterruptedException e) {
                }
            }
            //something has been written, but it has been read - wait
            while(numReadsSinceLastWrite > 0) {
                numReadsSinceLastWrite++;
                //AC:System.err.println("R: incrementing # reads to " + numReadsSinceLastWrite);
                if(eof) {
                    //AC:System.err.println("R: eof in second loop");
                    notifyAll();
                    return -1;
                }
                try {
                    //AC:System.err.println("R: waiting (second loop)");
                    notifyAll();
                    wait();
                    //AC:System.err.println("R: waking (second loop) (# reads = " + numReadsSinceLastWrite + ")");
                } catch (InterruptedException e) {
                }
            }
            //something has just been written - read
            //AC:System.err.println("R: reading char '" + writeBuf + "'");
            buf[offset] = writeBuf;
            numReadsSinceLastWrite = 1;
            //AC:System.err.println("R: # reads = 1");
            notifyAll();
            return 1;
        }
        
        @Override
        synchronized public void close() throws java.io.IOException {
        }
    }