java/ch/wlkl/wsh/Compiler.java

package ch.wlkl.wsh;

import static ch.wlkl.wsh.Parser.findEndStart;
import static ch.wlkl.wsh.Parser.notDo;
import static ch.wlkl.wsh.Parser.shName;
import static ch.wlkl.wsh.Parser.shWord;
import static ch.wlkl.wsh.Parser.space;
import static ch.wlkl.wsh.Parser.string;
import static ch.wlkl.wsh.Strings.unquote;
import static ch.wlkl.wsh.Strings.xQuote;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 * compiles shell or data syntax to javaCode and a temporary JavaClass, subclass of {@link Run} and loads it into the virtual machine.
 * 
 * <br><br><b>Attention:</b> java implements an old version of wsh, <a href="https://www.wlkl.ch/index.php/Inf/WshSyn">the rexx version</a> contains much innovation since then.
 * 
 * <h1>The main feature of this shell language</h1>
 * <ul>
 * <li>standard input and ouput with a unix-shell like syntax, to the file system, temporary buffers and heredata
 * <li>piping of statemts (using the output of one statement as input for the next)
 * <li>variable expansion in heredata or java
 * <li>integration with java, but the design is as much language independent as possible, there is a version z/OS REXX under construction
 * </ul>
 * 
 * <h1>Shell Syntax</h1>
 * <ul>
 * <li id="syData"><B>data = (spCom* | exprD) (nl exprD)* (nl spCom*)? ((stmt | input) data)*</B>
 * <br>data consists of literal data line interpersed with variable expansions, input from files or heredata and statements.
 *  Partial lines are ignored, if they consist only of spaces and comments.
 * <li id="syShell"><B>shell = pipe ? ('$;' pipe?)*</B>
 * <br>Pipes separated by $;. Caution: Semicolons without $ do not terminate a pipe but are part of the underlying language.
 * <li id="syExpr"><B>exprD = (( notDoNl | primary ) comment* )+</B>
 * <br>expressions consists of notDoNl interpreted litaral constants and primaries, comments are ignored.
 * <li><B>exprS = spCom* exprD spCom*</B>
 * <br>a flavour of expression ignoring leading and trailing spaces
 * <li><B>exprW = spCom* (( shWord | primary ) comment* )+ spCom*</B>
 * <br>a flavour of expression ignoring leading and trailing spaces and text only words
 * <li id=syLang><B>lang = (( notDoNl | primary ) comment* )+</B>
 * <br> syntactically the same as <B>exprD</B>, however <B>notDoNl</B> is interpreted as language source. This may yield a java expression or java statements. Comments are replaced by a single space.
 * <li id=syPrimary><B>primary = vars | string | '$(' lang '$)' | '$-[' data '$]' | '$-{' shell '$}' | $-compShell | $-compData</B>
 * <br>the $-comp... primaries compile standard input as shell, respectively data and return the resulting instance of {@link Run}
 * <li><B>vars = '$' shName | '${' ('?' | '&gt;')? exprW colCla? '}'</B>
 * <br>retrieves the contents of a variable from the variable pool, tests its existence or reads from std input, see <A hRef=#seVars>semantics</A>
 * <li><B>colCla =  spCom ':' spCom shWord</B>S
 * <li id=syInput><b>input  = '$&lt;' ioOpts exprS | hereData</b>
 * <li id=syHereData><b>hereData  = '$&lt;&lt;' ioOpts shWord sp* (nl notNl*)* nl shWord</b>
 * <li> <b>output = ('$&gt;' | '$&gt;&gt;' | '$&amp;') ioOpts exprS</b>  
 * <li> <b>ioOpts = '+'? '-'? ( '?' |  '#' | '&amp;' | '[' | '{' )? </b>
 * <br> option characters to specify the meaning of exprS fileName see <A href=#seIoOpts>semantics</A>
 * <li id=syPipe><B>pipe = ((input | output) spNlCom*)* (stmts ( '$|' colCla? stmts)*)?</b>
 * <br>this is execution order, syntax order is free
 * <li id=syStmts><B>stmts = ((stmt | lang) spNlCom*)+</b>
 * <li id=syStmt><b>stmt =  ass | '$@{' shell '$}' | '$[' data '$]'| '$$' exprS  | '$?' lang  |'$@for' exprW colCla? stmt | '$@run' primary</b>
 * <li><b>ass = '$=' exprW colCla? ( '=' exprS | '?' lang)</B>
 * </ul>
 * <h1>Lexicals</h1>
 * <ul>
 * <li><b>string = '$''' ((* notNl not '''' *) | '''''')* '''' | '$?' ((* notNl not '?' *) | '??')* '?''</b>
 * <br>a string enclosed in single or double quotes, prefixed by a dollar. Inside, two quotes stand for a single one.
 * <li><b>shName = (* starting with a letter followed by any number of digits or characters *)</b>
 * <li><b>shWord = (* 1 or more characters not sp, nl, {, }, =, ? or : *)</b>
 * <br>
 * <li><b>spNlCom = nl | sp | comment</b>
 * <li><b>spCom = sp | comment</b>
 * <li><b> comment = '$**' notNl* | '$*+' notNl* nl | '$*(' (comment | hereData | string | char)* '$*)'</b>
 * <br>
 * <li> notDoNl = (* one character from notNl but not '$' *)
 * <li> notNl = (* one character not nl *)
 * <li> nl = (* newLine character(s) or record switch *)
 * <li id="sySp"> sp = (* one space character ' ', tab etc., but not nl *)
 * </ul>
 * <h1>Semantics</h1>
 * The run of a program fragment has the following effects
 * <ul>
 * <li>it reads from standard input
 * <li>it writes to standard output
 * <li>it accesses and updates the variable pool
 * <li>runs java code either from <b>lang</b> constructs or by method calls from the accesses to the variable pool
 * </ul>
 * The semantics of the syntactical constructs
 * <ul>
 * <li><b>data</b> writes in the given sequence to standard output: expressions, the output of statements and the contents of inputs.
 * <li><b>shell</b> writes the output of its pipes to standard output.
 * <li><b>exprD, exprS, exprW</b> if it constists of a single primary returns this primary otherwise a String consisting of the concatenation of each primary and literal
 * <li><B>primary</B> yields a String or an instance of another java class.
 * <ul><li id=seVars><B>vars</B> retrieves the contents of a variable from the variable pool, tests its existence or reads from std input. Nesting is supported. The function depends on the options:
 * <ul>
 * <li> <B>?</B> returns a boolean, whether the variable named exprW is defined in the variable pool. 
 * <li> <B>&gt;</B> reads from standard input and puts the line read into the variable named exprW. returns false at end of file.
 * <li> <B>neither</B> returns the contents of the variable named exprW or fails if it is undefined
 * <li> <B>colCla</B> returns an object of the specified class, if not specified shell infers the type
 * </ul>  
 * <li><B>string</B> a string constant
 * <li><B>'$(' lang '$)'</B> lang must be a java expression. Yields the object (of any class) of this java expression.
 * <li><B>'$-[' data '$]'</B> yields a string: the concatenation of all lines of data, separated by a single space.
 * <li><B>$-{' shell '$}'</B> yields a string: shell is exececuted and the lines of its standard output concatenated, separated by a single space. 
 * <li><B>$-compShell, $-compData</B> compiles standard input as shell, respectively data yields the resulting instance of {@link Run}
 * </ul>
 * <li><b>colCla</b> designates the type (class) of an expression or declares the type of a variable. see <A hRef=#Typing>Typing</A>
 * <li><b>input</b> in a statment copies the specified {@link Read}, File, etc. to std output. As a io redirection of a pipe defines (std) input.
 * <li><b>hereData</b> the following source lines are used as input, depending on ioOpts literally or compiled.
 * <li><b>output</b> In a io redirection of a pipe defines (std) output.
 * <li id=seIoOpts><b>ioOpts</b> option characters to specify the meaning of the subsequent exprW. Which options are valid is contextdependent.
 * <br>general
 * <ul> 
 * <li> <B>+</B> for input: concatenenate this input with the next input, for an external FD reroute the following inputOutput to this external FD
 * <li> <B>-</B> do not open and close an inputOutput, do not allocate and deallocate an external FD
 * </ul>
 * not for hereData see {@link Cat#make(String, Object)}
 * <ul> 
 * <li> <B>?</B> exprS yields a languge Object that will be transformed to an {@link OpenClose} by {@link Cat#make(String, Object)}
 * <li> <B>&amp;</B> exprS is a String specification of an external FD
 * <li> <B>#</B> exprS is a name in the variable pool. If it does not exist, a new Buf&lt;String&gt; is put there. If it exists the retrieved value is interpreted as with the ? option.
 * <li> otherwise exprS is a filename of the external file system 
 * </ul>
 * only for hereData 
 * <ul> 
 * <li> <B>[</B> interpret this hereData as <A hRef=#syData>data</A>
 * <li> <B>{</B> interpret this hereData as <A hRef=#syShell>shell</A>
 * <li> otherwise this hereData is interpreted literally 
 * </ul>
 * <li><B>pipe</B> a pipe. input get std input (which may consist of a concatenation) of the whole pipe (, output std output. Additionally external FD may be allocated for the duration of the whole pipe. $| stands for the rerouting of std output of the previous stmts with std input of the following stmts.
 * <li><B>stmts</B> java code interpreted as java statements possibly interpersed with stmt. 
 * <li><B>stmt</B> a shell statement is
 * <ul><li><B>ass</B> assigns a variable in the variable pool.
 * <li><B>'$@{' shell '$}'</B> is a block simply executing the shell.
 * <li><B>'$@[' data '$]'</B> writes data to std output.
 * <li><B>'$$', '$?'</B> write the subsequent shell respectively language expression to std output.
 * <li><B>$@for</B> read std input until end of input. For each object execute the subsequent statement after assigning the object read to the variable name exprW.
 * <li><B>$@run</B> the expression must yield an instance of {@link Run} which is run.
 * </ul>
 * </ul>
 * <h1 id=Typing>Typing</h1>
 * <ul>
 * <li>The $| operator with the <B>colCla</B> defines the type of standard output within this pipe fragment.
 * <li>The of a constant variable name of the variable pool is remember when the <B>colCla</B> option is used in the assignment, or the type of the expression can be deduced. The type is remembered in the following text.
 * <li>the type of an expression of a single primary is the type of the primary, for all other expression it is String
 * </ul>
 * <h1>Tutorial</h1>
 * <h1>Plans, toDo</h1>
 * <ul>
 * <li>simple include facility and general compileTime processing and generating permanent named classes
 * <li> Env or EnvMan without typing?
 * </ul>
 * @author walter keller
 *
 */

public class Compiler extends Top {
    Parser parser = null;
    final JavaCmpLoad<Run> loader;
    final static String pckg = "temporary.shell";

    String envIn;
    String envOut;
    final String envRoot;
    final HashMap<String, String> envMap = new HashMap<String, String>();
    final HashMap<String, String> varType = new HashMap<String, String>();
    StringBuffer methods = new StringBuffer();
    int methodCnt = 0;

    public Compiler(JavaCmpLoad<?> ldr, Read<String> src) {
        loader = new JavaCmpLoad<Run>(ldr, null, null, null, Run.class);
        parser = new Parser(src);
        envIn = envOut = envRoot = envNew();
        envMap.put(envRoot, "String");
    }

    /**
     * return an instance of a newly compiled subclass of {@link Run}
     * @param type type for {@link #compileClass(char, String)}
     * @return the new instance of the newly compiled subclass of {@link Run}
     */
    public Run compileInstance(char type) {
        Class<? extends Run> cl = compileClass(type, null);
        try {
            return cl.newInstance();
        } catch (Exception e) {
            parser.fail("instanciate error class " + cl + ": " + e);
            return null;
        }
    }

    /**
     * return {@link #compileClass(char, String)} or null if an exception is catched
     */
    public Class<? extends Run> compileClassCatch(char type, String name) {
        try {
            return compileClass(type, name);
        } catch(AssertionError e) {
            Env.env().write("compile failed: " + e);
            return null;
        }
    }

    /**
     * compile a subclass of <tt>Run</tt>
     * @param type type for {@link #compileSrc(char)}
     * @param name the name of the class (may include package), if null a unique name is generated
     * @return    the compiled class
     */
    public Class<? extends Run> compileClass(char type, String name) {
        String run = compileSrc(type);
        Object[] importing = {getClass().getPackage()};
        Object[] implementing = {};
        StringBuffer code = new StringBuffer();
        int bx=0, ex;
        methods.append("\n@SuppressWarnings(\"unchecked\")\npublic void run(String... arg) {");
        methods.append(run == null ? "" : run).append("\n}");        

        while (bx < methods.length()&& 0 <= (ex = methods.indexOf(".env(?", bx))) {
            final int sx = methods.indexOf(" ", ex);
            final int cx = methods.indexOf(")", sx);
            final String io = "(" + envResolve(methods.substring(ex+5, sx)) + ") null, (" + envResolve(methods.substring(sx+1, cx)) + ") null";
            code.append(methods, bx, ex+5);
            if (! io.equals("(String) null, (String) null"))
                code.append(io);
            code.append(')');
            bx = cx + 1;
        }
        code.append(methods, bx, methods.length());
        if (name == null)
            name = loader.uniqueName("TempRun");
        return loader.makeClass(name, loader.classSource(importing, name, implementing, code));
    }

    /**
     * return the java code for shell or data interpretation of the complete current source 
     * @param type 's' for shell or 'd' for data
     * @return java code
     */
    private String compileSrc(char type) {
        String src, what, expec;
        final String oldEnvIn = envIn;
        final String oldEnvOut = envOut;
        if (type == 's') {
            what = "shell";
            expec = "pipe or $;";
            parser.spaceNlComment();
            src = shell();
        } else if (type == 'd') {
            what = "data";
            expec = "sExpression or block";
            src = data(false);
        } else {
            parser.fail("bad type " + type);
            return null;
        }
        if (! parser.atEnd()) 
            parser.fail(expec + " expected: compile " + what + " stopped before end of input");
        if (! (oldEnvIn.equals(envIn) && oldEnvOut.equals(envOut)))
            fail("after compile " + what + " environment = (" + envIn + ", " + envOut + ") not as before (" + oldEnvIn + ", " + oldEnvOut + ")");
        return src;
    }    

    /**
     * compile a <A hRef=#syData>data</A> clause
     * @param makeExpr if true return an expression otherwise statements
     * @return the compiled code
     */
    private String data(boolean makeExpr) {
        ArrayList<String> exprs = new ArrayList<String>();
        String text;
        String one;
        Compiler$Expr nd, io;
        boolean isExpr = makeExpr;
        while (true) {
            boolean aftEol = false;
            while (true) {
                text = "";
                do {
                    if (parser.next(notDo))
                        text += parser.tok;
                } while (parser.comment());
                nd = expr('d');
                boolean befEol = parser.nl();
                if (nd != null || (aftEol && befEol) || (text.length() > 0  && ! space.matcher(text).matches())) {
                    one = (text.length() <= 0 ? "" : xQuote(text) + (nd == null ? "" : " + " )) + (nd == null ? "" : nd.code);
                    exprs.add(isExpr ? null2EE(one) : writeExpr(one));
                }
                if (! befEol)
                    break;
                aftEol = true;
            }
            if (null != (one = stmt())) {
            } else if (null != (io = ioRedirection())) {
                if (! io.constant.startsWith("<") || io.constant.indexOf('+') >= 0)
                    parser.fail("simple input");
                /* create java to write the one inputReader to standard out */
                if (io.constant.startsWith("<<"))
                    one = io.code;
                else
                    one = "\n" + env() + ".writeAll(\"" + io.constant + "\", Cat.makeRead(\"" + io.constant + "\", " + io.code + "));";
            } else  {
                return isExpr ? Strings.cat(exprs, " + \" \" + ") : makeExpr ? stmts2expr(Strings.cat(exprs, ""), " ") : Strings.cat(exprs, "");
            } 
            if (isExpr) {
                isExpr = false;
                writeExpr(exprs);
            }
            exprs.add(one);
        }
    }

    /**
     * write the expression to std ouput
     * @param ex the expression to write
     * @return the java code writing the expression
     */
    private String writeExpr(String ex) {
        return "\n" + env() + ".write(" + null2EE(ex) + ");";
    }

    /**
     * replace all each expression by a write of this expression to std output
     * @param exprs
     */
    private void writeExpr(List<String> exprs) {
        for (int ix=0; ix < exprs.size(); ix ++)
            exprs.set(ix, writeExpr(exprs.get(ix)));
    }

    
    /**
     * return the java code that yields an expression which is the concatenation of the output of the passed statements
     * We move the statements to a method, which captures the output to a buffer. 
     * @param stmts
     * @param mid the string to concatenate between two output lines
     * @return javacode calling the method which executes the stmts
    
     */
    String stmts2expr(String stmts, String mid) {
        return addMethod("String", "\nEnv.push(new Env<String, String>(\">?\", new Buf<String>()));" + stmts + "\nreturn Strings.cat(((Buf<String>) (Env.pop())).contents, \"" + mid + "\");");
    }

    /**
     * return java code to yield a reader, containing the output of the statements.
     * We move the statements to a method, which captures the output to a buffer. 
     * @param stmts
     * @return the javacode to call the new method
     */
    String stmts2read(String stmts) {
        return addMethod("Read<String>", "\nEnv.push(new Env<String, String>(\">?\", new Buf<String>()));" + stmts + "\nreturn (Buf<String>) Env.pop();");
    }

    /**
     * create a method executing the passed code and return a call to the method.
     * @param cla the return type of the method 
     * @param code
     * @return javacode calling the method
     */
    String addMethod(String cla, String code) {
        String name = "method" + ++methodCnt;
        methods.append("\n").append(cla).append(" ").append(name).append("() {").append(code).append("\n}");
        return name + "()";
    }
    /**
     * compile an <A href=#syExpr>expr clause</A>.
     * <p>
     * Set isSpaceOnly to whether clause consists only of spaces and {@link Parser#comment()}.
     * @param ty 'd' exprD, 's' exprS, 'w' exprW 
     * @return a Node containing the compiled javaCode
     */
    private Compiler$Expr expr(char ty) {
        boolean strip = ty == 's' || ty == 'w';
        boolean first = true;
        String text;
        Compiler$Expr one;
        Compiler$Expr single = null;
        Compiler$Expr res = new Compiler$Expr("", "?c", "");

        if (strip)
            parser.spaceComment();
        while (true) {     // primary loop
            res.constant = text = "";
            while (true) {    // constant loop
                if (parser.next(ty == 'w' ? shWord : notDo)) {
                    text += parser.tok;
                } else if (parser.comment()) {                        
                } else if (null != (one = primary()) && "?c".equals(one.type)) {
                    res.constant += text + one.constant;
                    text = "";
                } else {
                    break;
                }
            }
            if (one == null && strip) 
                text = text.substring(0, findEndStart(text, space));
            res.constant += text;
            if (res.constant.length() > 0)
                res.code += (first ? "" : " + ") + xQuote(res.constant);
            if (one == null) {
                if (strip)
                    parser.spaceComment();
                if (first)
                    return res.code.length() <= 0 ? null : res;
                if (single != null && res.constant.length() <= 0 )
                    return single;
                res.constant = null;
                return res;
            }
            res.type = "String";
            if (single != null) {
                res.code = "\"\" + " + res.code;
                single = null;
            }
            if (res.code.length() <= 0)
                res.code = (single = one).code;
            else
                res.code += " + " + one.code;
            first = false;
        }
    }

    /**
     * compile a <A href=#syPrimary>primary clause</A>
     * 
     * @return a Node containing the compiled javaCode
     */
    private Compiler$Expr primary() {
        String one = null;
        Compiler$Expr nd = null;

        if (! parser.lit("$")) {
            return null;
        } else if (parser.next(string)) {
            return new Compiler$Expr(xQuote(one = unquote(parser.tok)), "String", one);
        } else if (parser.lit("(")) {
            one = checkNN(lang(), "jaString in $(...$)");
            if (! parser.lit("$)"))
                parser.fail("$) for end of $( jaExpression $) expected");
            return new Compiler$Expr("(" + one + ")", "?e");
        } else if (parser.lit("-[")) {
            one = data(true);
            if (! parser.lit("$]"))
                parser.fail("$] for end of $-[ shExpression expected");
            return new Compiler$Expr(one, "?e");
        } else if (parser.lit("-{")) {
            one = stmts2expr(shell(), " ");
            if (! parser.lit("$}"))
                parser.fail("$} for end of $-{ shell expected");
            return new Compiler$Expr(one, "?e");
        } else if (parser.lit("-cmpShell") || parser.lit("-cmpData")) {
            return new Compiler$Expr("Env.compile('" + (parser.tok.charAt(4) == 'D' ? 'd': 's') + "')", "Run");
        } else if (parser.lit("{")) {
            nd = varCla(parser.lit("?") || parser.lit(">") ? parser.tok.charAt(0) : 'g');
            if (! parser.lit("}"))
                parser.fail("closing } after ${ missing");            
            return nd;
        } else if ((nd = varCla('$')) != null) {
            return nd;
        } else {
            parser.charIx--;
            return null;
        }
    }

    /**
     * return a node for the variable access. Keep track of the variable name if it is a constant and infer the type
     * @param fun
     * @return a node for the compiled variable access
     */
    Compiler$Expr varCla(char fun) {
        Compiler$Expr exp = null;
        Compiler$Expr res = new Compiler$Expr("");
        if (fun == '$') {
            if (! parser.next(shName))
                return null;
            exp = new Compiler$Expr(xQuote(parser.tok), "?c", parser.tok);
        } else {
            parser.spaceComment();
            exp = expr('w');
            if (exp == null || exp.code.length() < 1)
                parser.fail("variable name expected");
            res.type = colClass();
        }

        if (exp.type.equals("?c")) {
            res.constant = exp.constant;
            if (fun == '=' && res.type != null)
                varType.put(res.constant, res.type);
            else if (fun == '>')
                varType.put(res.constant, res.type == null ? res.type=envResolve(envIn) : res.type);
            else if (res.type == null && varType.containsKey(res.constant))
                res.type = varType.get(res.constant);
        } else {
            res.type = null;            
        }

        if (fun == 'g' || fun == '$') {
            res.code = (res.type == null ? "" : "((" + res.type + ") ") + env() +".via(" + exp.code + (res.type == null ? ")" : "))");
        } else if (fun == '>') {
            res.code = env() + ".read(" + exp.code + ")";
            res.type =  "bool";
        } else if (fun == '?') {
            res.code = env() + ".hasKey(" + exp.code + (res.type == null ? ")" : ", " + res.type + ".class)");
            res.type = "bool";
        } else if (fun == '=') {
            res.code = env() + ".put(" + exp.code + (res.type == null ? ", " : ", (" + res.type + ") null, ");
        } else {
            fail("bad fun " + fun);
        }
        return res;
    }
    /**
     * compile a </A href=#syLang>lang clause</A>.
     * <p>
     * Text is interpreted as Java source. A comment is replaced by a space, if it the last generated character is not already a space character
     * @return the compile JavaSrc
     */
    private String lang() {
        StringBuffer code = new StringBuffer();
        Compiler$Expr nd;
        while (true) {
            if (parser.next(notDo)) 
                code.append(parser.tok);
            else if (null != (nd = primary())) 
                code.append(nd.code);
            else if (parser.comment())
                code.append(' ');
            else
                return code.length() <= 0 ? null : code.toString();
        }
    }

    /**
     * compile a ':' class clause
     * @return <, <shWrd>.class>, <, def> or "" 
     */
    private String colClass() {        
        if (! parser.lit(":")) 
            return null;
        parser.spaceComment();
        if (! parser.next(shWord)|| space.matcher(parser.tok).matches())
            parser.fail("class expected after :");
        String cl = parser.tok;
        parser.spaceComment();
        return cl;
    }

    /**
     * 
     * @return a Node, constant contains option, code specification of filename or object
     */
    private Compiler$Expr ioRedirection() {
        Compiler$Expr res = new Compiler$Expr("");
        if (parser.lit("$" + Cat.eExtFD)) {
            res.constant = Cat.eExtFDStr + parser.chars("+-");
            res.code = expr('s').code;
        } else if (parser.lit("$<") || parser.lit("$>")) {
            res.constant = parser.tok.substring(1);
            if (parser.lit(res.constant))
                res.constant += res.constant;        
            res.constant += parser.chars("+-?" + Cat.eBufVar);
            if (! res.constant.startsWith("<<")) {
                res.code = checkNN(expr('s'), "io expr expected").code;
            } else {
                char compType = parser.lit("[") ? 'd' : parser.lit("{") ? 's' : ' ';
                ArrayList<String> data = new ArrayList<String>();
                if (! parser.next(shName))
                    parser.fail("after $<< delimiter word");
                String stopper = parser.tok;
                parser.spaceComment();
                if (! parser.atEol())
                    parser.fail("spaceComment* nl expected after $<<" + stopper);
                while (parser.switchLine() && ! parser.src.startsWith(stopper))
                    data.add(parser.src);
                if (! parser.lit(stopper))
                    parser.fail("end word " + stopper + "expected after $<<");

                if (compType != ' ') {
                    Parser old = parser;
                    parser = new Parser(new Buf<String>(data));
                    res.code= compileSrc(compType);
                    parser = old;
                } else {
                    res.code = "";
                    for (String s : data)
                        res.code += "\n" + env() + ".write(" + xQuote(s) + ");";
                }
            } 
        } else {
            return null;
        }
        return res;
    }

    /**
     * compile a <A href=#syShell>shell syntax</A>
     * @return java statements
     */
    private String shell() {
        String one;
        StringBuffer code = new StringBuffer("");
        while (true) {
            if (null != (one = pipe()))
                code.append(one);
            if (! parser.lit("$;")) 
                return (code.length() > 0 ? code.toString() : null);
            parser.spaceNlComment();
        } 
    }

    /**
     * compile a <A href=#syPipe>pipe syntax</A>
     * @return java statements
     */
    private String pipe() {
        Compiler$Expr io1;
        ArrayList<String> stmts = new ArrayList<String>(); 
        String one = null;
        StringBuffer ios = new StringBuffer();
        final String envOldIn = envIn;
        final String envOldOut = envOut;
        String envNxt = envNew();
        boolean hasIos;

        while (true) {
            if (null != (io1 = ioRedirection())) {
                if (io1.constant.startsWith("<<")) {
                    io1.code = stmts2read(io1.code);
                    io1.constant = io1.constant.substring(1) + '?';
                }
                ios.append(ios.length() < 1 ? "\nEnv.push(new Env<String, String>(" : ", ");
                ios.append('"').append(io1.constant).append("\", ").append(io1.code);
            } else if (stmts.isEmpty()) {
                envOut = envNxt;
                one = stmts();
                envOut = envOldOut;
                if (null == one)
                    break;
                stmts.add(one);
            } else if (parser.lit("$|")) {
                parser.spaceNlComment();
                envMap.put(envNxt, colClass());
                envIn = envNxt;
                envOut = envNxt = envNew();
                stmts.add(checkNN(stmts(), "stmts expected after $| or $|: class"));
                envIn = envOldIn; 
                envOut = envOldOut; 
            } else {
                break;
            }
            parser.spaceNlComment();
        }
        if (io1 != null && io1.constant.indexOf('+') >= 0)
            parser.fail("pipe ends with open concatenation " + io1.constant + ", " + io1.code);

        if (! stmts.isEmpty())
            envMap.put(envNxt, envOldOut);

        if (! (hasIos = ios.length() > 0)) {
            if (stmts.isEmpty())
                return null;
            else if (stmts.size() == 1)
                return stmts.get(0);
        } else {
            if (stmts.isEmpty())
                stmts.add("\n" + env() + ".readWrite();");    
            ios.append("));");
        }

        if (stmts.size() <= 1) {
            ios.append(stmts.get(0));
        } else {
            ios.append("\nEnv.barBegin();").append(stmts.get(0));
            for (int ix=1; ix < stmts.size()-1; ix++) 
                ios.append("\nEnv.bar();").append(stmts.get(ix));
            ios.append("\nEnv.barLast();").append(stmts.get(stmts.size()-1)).append("\nEnv.barEnd();");
        }
        if (hasIos) 
            ios.append("\nEnv.pop();"); // wkTest eager???
        return ios.toString();
    }

    /**
     * Compile a <A href=#syStmts>stmts clause</A> containing java source and statemtens.
     * @return java statements
     */
    private String stmts () {
        String one;
        StringBuffer code = new StringBuffer("");
        boolean found = false;
        while(true) {
            if ((one = stmt()) != null) {
                if (one.length() > 0)
                    code.append(one);
            } else if (null != (one = lang())) {
                code.append('\n').append(one);
            } else {
                return found ?  code.toString() : null;
            }
            parser.spaceNlComment();
            found = true;
        }
    }

    /**
     * Compile a <A href=#syStmt>stmt</A>, which is a single shell statment.
     * @return java statements
     */
    private String stmt () {
        String one = null;        
        if (parser.lit("$=")) {        
            Compiler$Expr nd = varCla('=');
            if (parser.lit("=")){
                Compiler$Expr xx = expr('s');
                if (xx.type != null && xx.type.charAt(0) != '?' && nd.type == null && nd.constant != null) {
                    nd.code += "(" + xx.type + ")null, ";
                    varType.put(nd.constant, xx.type);
                }
                one = null2EE(xx == null || xx.code.length() < 1 ? null : xx.code);
            } else if (parser.lit("?")) {
                one = checkNN(lang(), "java Expression");
            } else {
                parser.fail("= or ? expected in assignement");
            }
            return "\n" + nd.code + one + ");";
        } else if ( parser.lit("$@{")) {
            parser.spaceNlComment();
            one = shell(); 
            if ( ! parser.lit("$}"))
                parser.fail("closing $} for $@{ block missing");
            return one == null ? "" : "\n{" + one + "\n}";
        } else if ( parser.lit("$[")) {
            one = data(false); 
            if ( ! parser.lit("$]"))
                parser.fail("closing $] for $[ block missing");
            return one == null ? "" : "\n{" + one + "\n}";
        } else if (parser.lit("$$")) {
            return "\n" + env() + ".write(" + expr('s').code + ");";
        } else if (parser.lit("$?")) {
            return "\n" + env() + ".write(" + lang() + ");";
        } else if ( parser.lit("$@for")) {
            return "\nwhile (" + varCla('>').code + ") {" + checkNN(stmt(), "statement after $@for var expected") + "\n}";
        } else if (parser.lit("$@run")) {
            return "\n(" + checkNN(expr('s'), "expression for $@run expected").code + ").run();";
        } else {
            return null;
        }
    }


    /**
     * if the first argument is null, parser.fail with the message in the second argument.
     * @return the first argument
     */
    private <T> T checkNN(T vl, String msg) {
        if (vl == null)
            parser.fail(msg + " expected here");
        return vl;
    }

    /** return the argument if not null, otherwise the empty String */
    private String null2EE(String vl) {
        return vl == null || vl.length() < 1 ? "\"\"" : vl;
    }    

    private String envNew() {
        String n = "?" + envMap.size();
        envMap.put(n, n);
        return n;
    }

    private String env() {
        return "Env.env(" + envIn + ' ' + envOut +")";
    }

    private String envResolve(String k) {
        if (! envMap.containsKey(k))
            fail("envResolve key " + k + " not in envMap");
        String v = envMap.get(k);
        if (k.equals(v))
            fail("circular env " + k);
        else if (v == null)
            envMap.put(k, v = "String");
        else if (v.length()> 0 && v.charAt(0) == '?')
            envMap.put(k, v = envResolve(v));
        return v;
    }
}

class Compiler$Expr {
    String code;
    String type;
    String constant;
    Compiler$Expr(String code, String type, String constant) {
        this.code = code;
        this.type = type;
        this.constant = constant;
    }
    Compiler$Expr(String code, String type) {
        this(code, type, null);
    }
    Compiler$Expr(String code) {
        this(code, null, null);
    }
}