view src/nabble/naml/compiler/Compiler.java @ 0:7ecd1a4ef557

add content
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 21 Mar 2019 19:15:52 -0600
parents
children
line wrap: on
line source

package nabble.naml.compiler;

import fschmidt.util.java.ArrayStack;
import fschmidt.util.java.CollectionUtils;
import fschmidt.util.java.ObjectUtils;
import fschmidt.util.java.Stack;
import nabble.naml.dom.Attribute;
import nabble.naml.dom.Cdata;
import nabble.naml.dom.Container;
import nabble.naml.dom.Element;
import nabble.naml.dom.ElementName;
import nabble.naml.dom.EmptyElement;
import nabble.naml.dom.Naml;
import nabble.naml.dom.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.CharArrayWriter;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;


final class Compiler {
	private static final Logger logger = LoggerFactory.getLogger(Compiler.class);

	private Traceable traceable = null;
	private final List<Traceable> traceables = new ArrayList<Traceable>();
	private final StackTrace stackTrace = new StackTrace() {

		public StackTraceElement pop() {
			StackTraceElement ste = super.pop();
			if( traceable != null )
				traceable.stackTrace.push(ste.intern());
			return ste;
		}

	};
	private final Stack<GenericNamespace> stack = new ArrayStack<GenericNamespace>();
	private final Program program;
	private final Map<String,List<Macro>> macros;
	private final Map<Macro,Macro> overrides;
	private final Set<String> staticNames;
	private final GenericNamespace[] base;

	static Template compile( Program program, String templateName, GenericNamespace[] base )
		throws CompileException
	{
		return new Compiler(program,base).compile(templateName);
	}

	private Compiler( Program program, GenericNamespace[] base ) {
		this.program = program;
		this.macros = program.macros();
		this.overrides = program.overrides();
		this.staticNames = program.staticNames();
		this.base = base;
	}

	private Template compile( String templateName )
		throws CompileException
	{
		for( GenericNamespace ns : base ) {
			pushWithExtensions(ns);
		}
		Macro macro = getTemplate(templateName);
		if( macro==null )
			return null;
		addMeaning(null,macro,null);
		StackTraceElement stackElem = new StackTraceElement(macro);
		stackTrace.push( stackElem );
		try {
			MacroScope macroScope = new MacroScope(macro);
			for( String s : macro.parameters ) {
				macroScope.addParamChunk( new ParamChunk(s) );
			}
			Chunk chunk = compileMacro( macroScope );
			if( chunk instanceof ErrorChunk )
				throw ((ErrorChunk)chunk).e;
			chunk = chunk.intern();
			Template template = new Template(this,program,chunk,classes(base),macro);
			return template;
		} finally {
			stackTrace.pop();
			cleanUpTraceables();
		}
	}

	private static Class<?>[] classes(GenericNamespace[] nss) {
		List<Class<?>> classes = new ArrayList<Class<?>>();
		for( GenericNamespace ns : nss ) {
			if( ns instanceof JavaNamespace ) {
				JavaNamespace jns = (JavaNamespace)ns;
				classes.add(jns.cls);
			}
		}
		return classes.toArray(new Class<?>[0]);
	}

	private void cleanUpTraceables() {
		for( Traceable t : traceables ) {
			t.cleanUp();
		}
	}

	private void addMeaning(ElementName.Part part,Meaning meaning,MacroScope macroScope) {
		program.addMeaning(part,meaning,macroScope,base);
	}

	private int pushWithExtensions(GenericNamespace gns) {
		stack.push(gns);
		if( !(gns instanceof JavaNamespace) )
			return 1;
		JavaNamespace ns = (JavaNamespace)gns;
		List<JavaNamespace> extensions = program.extensionMap().get(ns.cls);
		if( extensions == null ) {
			return 1;
		}
		for( JavaNamespace extension : extensions ) {
			stack.push(extension);
		}
		return 1 + extensions.size();
	}

	private int push(JavaCall javaCall,String param) {
		if( javaCall==null || !javaCall.isScoped(param) )
			return 0;
		return pushWithExtensions(JavaNamespace.getNamespace(javaCall.scopedType()));
	}

	private int push(Macro macro) throws CompileException {
		if( macro.type != Macro.Type.NAMESPACE )
			return 0;
		MacroNamespace ns = new MacroNamespace(macro,stackTrace,macros);
		stack.push(ns);
		return 1;
	}

	private void pop(int n) {
		for( ; n > 0; n-- ) {
			stack.pop();
		}
	}

	private int whereInStack(String namespace) {
		if( namespace == null )
			throw new NullPointerException("namespace is null");
		boolean isTop = true;
		for( int i = stack.size() - 1; i>=0; i-- ) {
			GenericNamespace ns = stack.get(i);
			if( (isTop || ns.isGlobal()) && ns.names().contains(namespace) )
				return i;
			if( isTop && !ns.isTransparent() )
				isTop = false;
		}
		return -1;
	}

	private boolean hasNamespace(String namespace) {
		return whereInStack(namespace) != -1;
	}

	private String accessibleStack() {
		final int top = stack.size() - 1;
		boolean isTop = true;
		List<GenericNamespace> list = new ArrayList<GenericNamespace>();
		for( int i=top; i>=0; i-- ) {
			GenericNamespace ns = stack.get(i);
			if( ns.isGlobal() || isTop ) {
				list.add(ns);
				if( isTop && !ns.isTransparent() )
					isTop = false;
			}
		}
		Collections.reverse(list);
		return list.toString();
	}

	int whereInStack(Class<?> cls)
		throws CompileException
	{
		boolean isTop = true;
		for( int i = stack.size() - 1; i>=0; i-- ) {
			GenericNamespace gns = stack.get(i);
			if( !(gns instanceof JavaNamespace) )
				continue;
			JavaNamespace ns = (JavaNamespace)gns;
			if( !isTop && !ns.isGlobal() )
				continue;
			if( cls.isAssignableFrom(ns.cls) )
				return i;
			if( isTop && !ns.isTransparent() )
				isTop = false;
		}
		throw new CompileException(stackTrace,"required namespace '"+JavaNamespace.getNamespace(cls)+"' not found in "+accessibleStack()+"  stack = "+stack);
	}

	private Macro getTemplate(String templateName)
		throws CompileException
	{
		Set<String> namespacesOnStack = new HashSet<String>();
		for( GenericNamespace ns : stack ) {
			namespacesOnStack.addAll( ns.names() );
		}
		List<Macro> candidates = macros.get(templateName);
		if( candidates == null )
			return null;
		Macro rtn = null;
		for( Macro candidate : candidates ) {
			if( namespacesOnStack.containsAll( candidate.requiredNamespaces ) ) {
				if( rtn != null )
					throw conflict(candidate,rtn);
				rtn = candidate;
			}
		}
		return rtn;
	}

	private Chunk compileMacro(MacroScope macroScope)
		throws CompileException
	{
		Macro macro = macroScope.macro;
		for( String ns : macro.requiredNamespaces ) {
			if( !hasNamespace(ns) )
				throw new CompileException(stackTrace,"required namespace '"+ns+"' for "+new StackTraceElement(macro)+" not found in "+accessibleStack()+"  stack = "+stack);
		}
		if( macroScope.parentScope != null && macro.type==Macro.Type.SUBROUTINE && program.getMacroWhichOverrides(macro)==null )
			return compileSubroutine(macro,macroScope);
		int pushed = push(macro);
		try {
			Chunk chunk = consolidate( compile( macro.naml, macroScope ) );
			chunk = trimChunk(chunk);
			if( !(chunk instanceof ErrorChunk) && macroScope.hasVars )
				chunk = new VarScope(chunk,macroScope);
			return chunk;
		} catch(StackOverflowError e) {
			throw new CompileException(stackTrace,"stack overflow, probably recursive call");
		} finally {
			pop(pushed);
		}
	}

	private Chunk compileSubroutine(Macro macro,MacroScope macroScope)
		throws CompileException
	{
		Map<String,Stringable> argMap = new HashMap<String,Stringable>();
		for( String argName : macroScope.getArgNames() ) {
			Naml argNaml = macroScope.getArg(argName);
			Chunk argValue = consolidate( compile(argNaml,macroScope.parentScope) );
			if( argValue == Chunk.NULL )
				continue;
			if( argValue instanceof ErrorChunk )
				return argValue;
			Stringable s = argValue==emptyChunk ? new StringableString("")
				: argValue instanceof StringChunk ? new StringableString(((StringChunk)argValue).s)
				: new StringableChunk(argValue)
			;
			argMap.put(argName,s);
		}
		GenericNamespace[] baseSub = new GenericNamespace[macro.requiredNamespaces.size()];
		List<Integer> iStackList = new ArrayList<Integer>();
		for( int i=0; i<baseSub.length; i++ ) {
			String namespace = macro.requiredNamespaces.get(i);
			int iStack = whereInStack(namespace);
			if( iStack == -1 )
				throw new CompileException(stackTrace,"namespace needed for subroutine '"+namespace+"' not found in "+accessibleStack()+"  stack = "+stack);
			GenericNamespace ns = stack.get(iStack);
			baseSub[i] = ns;
			if( ns instanceof JavaNamespace )
				iStackList.add( iStack(iStack) );
		}
		int[] aiStack = new int[iStackList.size()];
		for( int i=0; i<aiStack.length; i++ ) {
			aiStack[i] = iStackList.get(i);
		}
		return new Subroutine(this,argMap,aiStack,baseSub,stackTrace.peek().commandName());
	}


	private static final ElementName.Part nPart = new ElementName.Part("n").intern();
	private static final StringChunk closingTag = new StringChunk(">");
	private static final StringChunk closingEmptyTag = new StringChunk("/>");

	private List<Chunk> compile(Naml naml,MacroScope macroScope)
		throws CompileException
	{
		final List<Chunk> chunks = new ArrayList<Chunk>();
		for( Object obj : naml ) {
			if( obj instanceof Element ) {
				Element element = (Element)obj;
				final ElementName name = element.name();
				List<ElementName.Part> parts = name.parts();
				if( parts.size() > 1 && parts.get(0) == nPart ) {
					Chunk chunk = compileElement(element,macroScope);
					chunks.add(chunk);
					continue;
				} else if( parts.size() == 1 && parts.get(0).text() == "t" ) {
					Chunk chunk = compileTranslation(element,macroScope);
					chunks.add(chunk);
					continue;
				}
				if( !staticNames.contains(name.toLowerCaseString()) ) {
					stackTrace.push( new StackTraceElement(macroScope.source(),element) );
					try {
						throw new CompileException(stackTrace,"invalid static tag: "+name);
					} finally {
						stackTrace.pop();
					}
				}
				chunks.add( new StringChunk( "<" + element.name() ) );
				for( Attribute attr : element.attributes() ) {
					Chunk valueChunk = consolidate( compile( attr.value(), macroScope ) );
					if( valueChunk instanceof ErrorChunk ) {
						chunks.add( valueChunk );
					} else if( valueChunk == emptyChunk ) {
						chunks.add( new StringChunk( attr.toString("") ) );
					} else if( valueChunk instanceof StringChunk ) {
						StringChunk stringChunk = (StringChunk)valueChunk;
						chunks.add( new StringChunk( attr.toString(stringChunk.s) ) );
					} else {
						StackTraceElement stackTraceElement = new StackTraceElement(macroScope.source(),element,("[attr - "+attr.name()+"]"));
						stackTrace.push(stackTraceElement);
						try {
							chunks.add( new DynamicAttr( attr, valueChunk ) );
						} finally {
							stackTrace.pop();
						}
					}
				}
				String s = element.spaceAtEndOfOpeningTag();
				if( s.length() > 0 )
					chunks.add( new StringChunk(s) );
				if( element instanceof Container ) {
					chunks.add( closingTag );
					Container container = (Container)element;
					chunks.addAll( compile(container.contents(),macroScope) );
					chunks.add( new StringChunk(container.closingTag()) );
				} else {
					chunks.add( closingEmptyTag );
				}
				continue;
			} else if( obj instanceof Cdata ) {
				Cdata cdata = (Cdata)obj;
				chunks.add( new StringChunk(cdata.text()) );
				continue;
			}
			String s = obj.toString();
			if( s.length() > 0 )
				chunks.add( new StringChunk(s) );
		}
		return chunks;
	}

	private Chunk compileTranslation(Element element,MacroScope macroScope)
		throws CompileException
	{
		stackTrace.push( new StackTraceElement(macroScope.source(),element) );
		try {
			if( !(element instanceof Container) )
				throw new CompileException(stackTrace,"'t' tag may not be empty");
			if( element.name().endsWithDot() )
				throw new CompileException(stackTrace,"'t' tag may not end with dot");
		} finally {
			stackTrace.pop();
		}
		Container container = (Container)element;
		Naml contents = container.contents();
		String macroName = Macro.translationMacroName(contents,null);
		stackTrace.push( new StackTraceElement(macroScope.source(),element,macroName) );
		try {
			List<Macro> macroList = macros.get(macroName);
			if( macroList == null ) {
				contents = removeTranslationArgs(macroScope.source(),contents);
				return consolidate( compile(contents,macroScope) );
			}
			if( macroList.size() != 1 )
				throw new RuntimeException("invalid - "+macroList);
			Macro macro = macroList.get(0);
			MacroScope newScope = macroScope.newScope(macro);
			getTranslationArgs(macroScope.source(),contents,newScope);
			addMeaning(container.name().parts().get(0),macro,macroScope);
			return getMacroChunk2(newScope);
		} finally {
			stackTrace.pop();
		}
	}

	private Naml removeTranslationArgs(Source source,Naml oldContents) throws CompileException {
		boolean changed = false;
		Naml newContents = new Naml();
		for( Object obj : oldContents ) {
			if( obj instanceof Element ) {
				Element element = (Element)obj;
				ElementName name = element.name();
				List<ElementName.Part> parts = name.parts();
				if( parts.size() >= 2 && parts.get(0).text() == "t" ) {
					changed = true;
					stackTrace.push( new StackTraceElement(source,element) );
					try {
						if( parts.size() == 2 ) {
							if( name.endsWithDot() )
								throw new CompileException(stackTrace,"translation argument cannot end with dot");
							if( !(element instanceof Container) )
								throw new CompileException(stackTrace,"translation argument cannot be empty");
							Container container = (Container)element;
							newContents.addAll( container.contents() );
						} else {
							List<ElementName.Part> newParts = new ArrayList<ElementName.Part>();
							newParts.add(nPart);
							newParts.addAll( parts.subList(2,parts.size()) );
							ElementName newName = new ElementName(name.endsWithDot(),newParts);
							if( element instanceof Container ) {
								Container container = (Container)element;
								Naml newContainerContents = removeTranslationArgs(source,container.contents());
								Element newElement = new Container(newName,element.attributes(),"",element.lineNumber(),newContainerContents,"");
								newContents.add(newElement);
							} else {
								Element newElement = new EmptyElement(newName,element.attributes(),"",element.lineNumber());
								newContents.add(newElement);
							}
						}
					} finally {
						stackTrace.pop();
					}
					continue;
				}
				if( element instanceof Container ) {
					Container oldContainer = (Container)element;
					Naml oldContainerContents = oldContainer.contents();
					Naml newContainerContents = removeTranslationArgs(source,oldContainerContents);
					if( newContainerContents != oldContainerContents ) {
						changed = true;
						Container newContainer = new Container(oldContainer.name(),oldContainer.attributes(),"",oldContainer.lineNumber(),newContainerContents,"");
						newContents.add(newContainer);
						continue;
					}
				}
			}
			newContents.add(obj);
		}
		return changed ? newContents : oldContents;
	}

	private void getTranslationArgs(Source source,Naml oldContents,MacroScope newScope) throws CompileException {
		for( Object obj : oldContents ) {
			if( obj instanceof Element ) {
				Element element = (Element)obj;
				ElementName name = element.name();
				List<ElementName.Part> parts = name.parts();
				if( parts.size() >= 2 && parts.get(0).text() == "t" ) {
					String argName = parts.get(1).text();
					stackTrace.push( new StackTraceElement(source,element) );
					try {
						if( parts.size() == 2 ) {
							if( name.endsWithDot() )
								throw new CompileException(stackTrace,"translation argument cannot end with dot");
							if( !(element instanceof Container) )
								throw new CompileException(stackTrace,"translation argument cannot be empty");
							Container container = (Container)element;
							newScope.addArg( argName, container.contents() );
						} else {
							newScope.addArg( argName, getMacroMultiDotArg(element,2,true) );
						}
					} finally {
						stackTrace.pop();
					}
					continue;
				}
				if( element instanceof Container ) {
					Container container = (Container)element;
					getTranslationArgs(source,container.contents(),newScope);
					continue;
				}
			}
		}
	}

	private Chunk compileElement(Element element,MacroScope macroScope)
		throws CompileException
	{
		ElementName name = element.name();
		try {
			if( name.parts().size() == 2 ) {
				return singleMethodTag(element,macroScope);
			} else {
				return multiMethodTag(element,1,macroScope);
			}
		} catch(CompileMethodException e) {
			return new ErrorChunk(e);
		}
	}

	private Chunk singleMethodTag(Element element,MacroScope macroScope)
		throws CompileException
	{
		ElementName.Part part = element.name().parts().get(1);
		String cmdName = part.text();
		StackTraceElement stackElem = new StackTraceElement(macroScope.source(),element,cmdName);
		stackTrace.push(stackElem);
		try {
			Chunk chunk = getMacroArgChunk(cmdName,macroScope);
			if( chunk != null ) {
				if( element.name().endsWithDot() )
					throw new CompileException(stackTrace,"macro parameter cannot take a dot_parameter");
				if( element instanceof Container )
					throw new CompileException(stackTrace,"parameter must be empty element, tag must end with '/'");
				if( !element.attributes().isEmpty() )
					throw new CompileException(stackTrace,"no arguments are allowed in a parameter");
				addMeaning(part,ParamMeaning.INSTANCE,macroScope);
				return chunk;
			}
			Call call = getCommandCall(cmdName,macroScope);
			if( call instanceof Macro ) {
				Macro macro = (Macro)call;
				addMeaning(part,macro,macroScope);
				return getMacroChunk(macro,element,macroScope,false);
			}
			JavaCall javaCall = (JavaCall)call;
			stackElem.setMethod(javaCall.getMethod());
			Map<String,Chunk> dynamicArgs = attributes(element,javaCall,macroScope);
			if( element.name().endsWithDot() ) {
				getDotArg( (Container)element, macroScope, javaCall, dynamicArgs );
			} else if( element instanceof Container ) {
				getTagArgs( (Container)element, macroScope, javaCall, dynamicArgs );
			}
			addMeaning(part,javaCall.javaCommand,macroScope);
			return methodChunk(element,macroScope,javaCall,dynamicArgs);
		} finally {
			stackTrace.pop();
		}
	}

	private Chunk multiMethodTag(Element element,int iCmd,MacroScope macroScope)
		throws CompileException
	{
		ElementName.Part part = element.name().parts().get(iCmd);
		String methodName = part.text();
		StackTraceElement stackElem = new StackTraceElement(macroScope.source(),element,methodName);
		stackTrace.push(stackElem);
		try {
			Chunk chunk = getMacroArgChunk(methodName,macroScope);
			if( chunk != null ) {
				if( iCmd != element.name().parts().size() - 1 || element.name().endsWithDot() )
					throw new CompileException(stackTrace,"macro parameter cannot take a dot_parameter");
				if( !element.attributes().isEmpty() )
					throw new CompileException(stackTrace,"no arguments are allowed in an parameter");
				addMeaning(part,ParamMeaning.INSTANCE,macroScope);
				return chunk;
			}
			Call call = getCommandCall(methodName,macroScope);
			if( iCmd == element.name().parts().size() - 1 ) {
				if( call instanceof Macro ) {
					Macro macro = (Macro)call;
					addMeaning(part,macro,macroScope);
					return getMacroChunk(macro,element,macroScope,true);
				}
				JavaCall javaCall = (JavaCall)call;
				stackElem.setMethod(javaCall.getMethod());
				Map<String,Chunk> dynamicArgs = attributes(element,javaCall,macroScope);
				if( element.name().endsWithDot() )
					getDotArg( (Container)element, macroScope, javaCall, dynamicArgs );
				addMeaning(part,javaCall.javaCommand,macroScope);
				return methodChunk(element,macroScope,javaCall,dynamicArgs);
			}
			if( call instanceof Macro ) {
				Macro macro = (Macro)call;
				if( macro.dotParam==null )
					throw new CompileException(stackTrace,"macro '"+macro.name+"' doesn't support multi-method tags");
				MacroScope newScope = macroScope.newScope(macro);
				newScope.addArg(macro.dotParam,getMacroMultiDotArg(element,iCmd+1,false));
				if( !element.name().endsWithDot() && iCmd==1 && element instanceof Container ) {
					getMacroTagArgs( macro, (Container)element, newScope );
				}
				addMeaning(part,macro,macroScope);
				return getMacroChunk2(newScope);
			}
			JavaCall javaCall = (JavaCall)call;
			stackElem.setMethod(javaCall.getMethod());
			String dotParam = javaCall.cmdSpec().dotParameter;
			if( dotParam==null )
				throw new CompileException(stackTrace,"method '"+methodName+"' in "+javaCall.method().getDeclaringClass()+" doesn't support multi-method tags");
			int pushed = push(javaCall,dotParam);
			try {
				chunk = multiMethodTag(element,iCmd+1,macroScope);
				chunk = trimChunk(chunk);
			} finally {
				pop(pushed);
			}
			Map<String,Chunk> dynamicArgs = new HashMap<String,Chunk>();
			if( iCmd==1 && element instanceof Container && !element.name().endsWithDot() ) {
				getTagArgs( (Container)element, macroScope, javaCall, dynamicArgs );
			}
			if( dynamicArgs.put( javaCall.cmdSpec().dotParameter, chunk ) != null )
				throw new CompileException(stackTrace,"dot_parameter duplicated as regular argument");
			addMeaning(part,javaCall.javaCommand,macroScope);
			return methodChunk(element,macroScope,javaCall,dynamicArgs);
		} finally {
			stackTrace.pop();
		}
	}

	private Map<String,Chunk> attributes(Element element,JavaCall javaCall,MacroScope macroScope) throws CompileException {
		Map<String,Chunk> dynamicArgs = new HashMap<String,Chunk>();
		for( Attribute attr : element.attributes() ) {
			String name = attr.name();
			int pushed = push(javaCall,name);
			try {
				dynamicArgs.put( name, consolidate( compile(attr.value(),macroScope) ) );
			} finally {
				pop(pushed);
			}
		}
		return dynamicArgs;
	}

	private Chunk getMacroArgChunk(String cmdName,MacroScope macroScope)
		throws CompileException
	{
		ParamChunk paramChunk = macroScope.getParamChunk(cmdName);
		if( paramChunk != null )
			return paramChunk;
		Naml macroArg = macroScope.getArg(cmdName);
		if( macroArg == null )
			return null;
		return consolidate( compile(macroArg,macroScope.parentScope) );
	}

	private Chunk getMacroChunk(Macro macro,Element element,MacroScope macroScope,boolean isMulti)
		throws CompileException
	{
		MacroScope newScope = macroScope.newScope(macro);
		for( Attribute attr : element.attributes() ) {
			String name = attr.name();
			if( !macro.parameters.contains(name) )
				throw new CompileException(stackTrace,"parameter '"+name+"' not allowed, only use "+macro.parameters);
			newScope.addArg(name,attr.value());
		}
		if( element instanceof Container ) {
			Container container = (Container)element;
			ElementName name = container.name();
			if( name.endsWithDot() ) {
				if( macro.dotParam==null )
					throw new CompileException(stackTrace,"macro '"+macro.name+"' doesn't have a dot_parameter");
				Naml naml = container.contents();
				naml = Macro.trim(naml);
				newScope.addArg( macro.dotParam, naml );
			} else if(!isMulti) {
				getMacroTagArgs( macro, container, newScope );
			}
		}
		return getMacroChunk2(newScope);
	}

	private void getMacroTagArgs(Macro macro,Container container,MacroScope newScope)
		throws CompileException
	{
		for( Object obj : container.contents() ) {
			if( obj instanceof Element ) {
				Element element = (Element)obj;
				ElementName name = element.name();
				boolean isSingle = name.parts().size() == 1;
				String argName = name.parts().get(0).text();
				if( !macro.parameters.contains(argName) )
					throw new CompileException(stackTrace,"tag '"+argName+"' is not an allowed here, only these are allowed: "+macro.parameters);
				if( isSingle ) {
					if( !(element instanceof Container) )
						throw new CompileException(stackTrace,"empty tag '"+argName+"' not allowed here, argument expected");
					if( !element.attributes().isEmpty() )
						throw new CompileException(stackTrace,"tag '"+argName+"' may not have arguments");
					Container argContainer = (Container)element;
					Naml naml = argContainer.contents();
					naml = Macro.trim(naml);
					newScope.addArg(argName,naml);
				} else {
					newScope.addArg(argName,getMacroMultiDotArg(element,1,true));
				}
			} else if( !(obj instanceof String) ) {
				throw new CompileException(stackTrace,obj.getClass().getName()+" not allowed here");
			}
		}
	}

	private static Naml getMacroMultiDotArg(Element element,int i,boolean allowArgContents) {
		ElementName name = element.name();
		List<ElementName.Part> parts = new ArrayList<ElementName.Part>();
		parts.add(nPart);
		parts.addAll( name.parts().subList(i,name.parts().size()) );
		ElementName newName = new ElementName(name.endsWithDot(),parts);
		Element newElement;
		if( name.endsWithDot() || allowArgContents && element instanceof Container ) {
			Container argContainer = (Container)element;
			newElement = new Container(newName,element.attributes(),"",element.lineNumber(),argContainer.contents(),"");
		} else {
			newElement = new EmptyElement(newName,element.attributes(),"",element.lineNumber());
		}
		Naml naml = new Naml();
		naml.add(newElement);
		return naml;
	}

	private static final Naml nullNaml;
	static {
		try {
			nullNaml = Naml.parser().parse("<n.null/>");
		} catch(ParseException e) {
			logger.error("",e);
			System.exit(-1);
			throw new RuntimeException(e);
		}
	}

	private Chunk getMacroChunk2(MacroScope newScope)
		throws CompileException
	{
		Set<String> parameters = new HashSet<String>(newScope.macro.parameters);
		parameters.removeAll(newScope.getArgNames());
		for( String param : parameters ) {
			newScope.addArg( param, nullNaml );
		}
		return compileMacro(newScope);
	}

	private void getDotArg(Container container,MacroScope macroScope,JavaCall javaCall,Map<String,Chunk> dynamicArgs)
		throws CompileException
	{
		String dotParam = javaCall.cmdSpec().dotParameter;
		if( dotParam==null )
			throw new CompileException(stackTrace,"command '"+javaCall.name()+"' doesn't support multi-method tags");
		int pushed = push(javaCall,dotParam);
		try {
			Chunk block = consolidate( compile( container.contents(), macroScope ) );
			block = trimChunk(block);
			if( dynamicArgs.put( dotParam, block ) != null )
				throw new CompileException(stackTrace,"dot_parameter duplicated as regular argument");
		} finally {
			pop(pushed);
		}
	}

	private void getTagArgs(Container container,MacroScope macroScope,JavaCall javaCall,Map<String,Chunk> dynamicArgs)
		throws CompileException
	{
		for( Object obj : container.contents() ) {
			if( obj instanceof Element ) {
				Element element = (Element)obj;
				ElementName name = element.name();
				boolean isSingle = name.parts().size() == 1;
				if( isSingle && !(element instanceof Container) )
					throw new CompileException(stackTrace,"empty " + notAllowed(macroScope,element,javaCall) );
				String argName = name.parts().get(0).text();
				if( !javaCall.cmdSpec().parameters.contains(argName) )
					throw new CompileException(stackTrace,notAllowed(macroScope,element,javaCall));
				if( isSingle && !element.attributes().isEmpty() )
					throw new CompileException(stackTrace,"tag "+argName+" may not have arguments");
				int pushed = push(javaCall,argName);
				try {
					Chunk block;
					if( isSingle ) {
						Container argContainer = (Container)element;
						block = consolidate( compile( argContainer.contents(), macroScope ) );
					} else {
						block = compileElement( element, macroScope );
					}
					block = trimChunk(block);
					if( dynamicArgs.put( argName, block ) != null )
						throw new CompileException(stackTrace,"duplicate argument: "+argName);
				} catch(TemplateRuntimeException e) {
					throw e;
				} catch(RuntimeException e) {
					throw new TemplateRuntimeException("in "+name,e);
				} finally {
					pop(pushed);
				}
			} else if( !(obj instanceof String) ) {
				throw new CompileException(stackTrace,obj.getClass().getName()+" not allowed here");
			}
		}
	}

	private static String notAllowed(MacroScope macroScope,Element element,JavaCall javaCall) {
		Set<String> parameters = javaCall.cmdSpec().parameters;
		StringBuilder buf = new StringBuilder();
		buf
			.append( "tag " )
			.append( new StackTraceElement(macroScope.source(),element) )
			.append( " is not allowed here, " )
		;
		if( parameters.isEmpty() ) {
			buf.append( "no tags allowed" );
		} else {
			buf.append("only tags ");
			Iterator<String> iter = parameters.iterator();
			buf
				.append( "<" )
				.append( iter.next() )
				.append( ">, " )
			;
			while( iter.hasNext() ) {
				buf
					.append( ", <" )
					.append( iter.next() )
					.append( ">" )
				;
			}
			buf.append( " are allowed here" );
		}
		if( javaCall.cmdSpec().dotParameter != null )
			buf.append( ", or you may have forgotten the dot at the end of the tag name" );
		buf
			.append( " in tag element '" )
			.append( javaCall.name() )
			.append( "'" )
		;
		return buf.toString();
	}

	private Chunk methodChunk(Element element,MacroScope macroScope,JavaCall javaCall,Map<String,Chunk> dynamicArgs)
		throws CompileException
	{
		Chunk methodChunk = methodChunk2(element,macroScope,javaCall,dynamicArgs);
		if( methodChunk instanceof Block ) {
			Block block = (Block)methodChunk;
			Chunk chunk = block.prerun(new CompileTimeRunState());
			if( chunk != null ) {
				return chunk;
			}
		}
		for( Chunk chunk : dynamicArgs.values() ) {
			if( chunk instanceof ErrorChunk )
				return chunk;
		}
		return methodChunk;
	}

	private Chunk methodChunk2(Element element,MacroScope macroScope,JavaCall javaCall,Map<String,Chunk> dynamicArgs)
		throws CompileException
	{
		Map<String,String> staticArgs = new HashMap<String,String>();
		for( Iterator<Map.Entry<String,Chunk>> iter = dynamicArgs.entrySet().iterator(); iter.hasNext(); ) {
			Map.Entry<String,Chunk> entry = iter.next();
			Chunk chunk = entry.getValue();
			if( chunk == emptyChunk ) {
				staticArgs.put( entry.getKey(), "" );
				iter.remove();
			} else if( chunk instanceof StringChunk ) {
				StringChunk sc = (StringChunk)entry.getValue();
				staticArgs.put( entry.getKey(), sc.s );
				iter.remove();
			}
		}
		if( javaCall.cmdSpec().removeNulls )
			removeNulls(dynamicArgs);
		javaCall.cmdSpec().check(staticArgs,dynamicArgs,stackTrace);
		if( javaCall.name() == "var" ) {
			return new GetVar(macroScope,staticArgs,dynamicArgs);
		}
		if( javaCall.name() == "set_var" ) {
			return new SetVar(macroScope,staticArgs,dynamicArgs);
		}
		if( javaCall.name() == "uplevel_var" ) {
			return new GetVar(macroScope.parentScope,staticArgs,dynamicArgs);
		}
		if( javaCall.name() == "uplevel_set_var" ) {
			return new SetVar(macroScope.parentScope,staticArgs,dynamicArgs);
		}
		Map<Class,Integer> inStack = new HashMap<Class,Integer>();
		for( Class cls : javaCall.cmdSpec().requiredInStack ) {
			inStack.put( cls, iStack(whereInStack(cls)) );
		}
		if( javaCall.isScoped() ) {
			if( javaCall.cmdSpec().scopedParameters == null )
				throw new CompileException(stackTrace,"a scoped method must specify scoped arguments");
			Map<String,Chunk> dynamicArgs2 = new HashMap<String,Chunk>(dynamicArgs);
			Map<String,Chunk> scopedArgs = new HashMap<String,Chunk>();
			for( String scopedParam : javaCall.cmdSpec().scopedParameters ) {
				Chunk chunk = dynamicArgs2.remove(scopedParam);
				if( chunk != null )
					scopedArgs.put(scopedParam,chunk);
			}
			return new Scoped(this,javaCall,inStack,staticArgs,dynamicArgs2,scopedArgs);
		}
		return new Block(this,javaCall,inStack,staticArgs,dynamicArgs);
	}

	private static void removeNulls(Map<String,Chunk> dynamicArgs) {
		for( Iterator<Chunk> iter = dynamicArgs.values().iterator(); iter.hasNext(); ) {
			if( iter.next() == Chunk.NULL )
				iter.remove();
		}
	}

	private int iStack(int i) {
		if( !(stack.get(i) instanceof JavaNamespace) )
			throw new RuntimeException();
		int n = 0;
		for( int j=i; j>=0; j-- ) {
			if( !stack.get(j).isGlobal() ) {
				final int size = stack.size();
				for( int k=i; k<size; k++ ) {
					if( stack.get(k) instanceof JavaNamespace )
						n++;
				}
				return -n;
			}
		}
		for( int k=0; k<i; k++ ) {
			if( stack.get(k) instanceof JavaNamespace )
				n++;
		}
		return n;
	}

	private Call getCommandCall(String cmdName,MacroScope macroScope)
		throws CompileException
	{
		Call call = null;
		boolean isTop = true;
		Set<Class> nsSet = new HashSet<Class>();
		Set<String> namespacesOnStack = new HashSet<String>();
		for( int i = stack.size() - 1; i>=0; i-- ) {
			GenericNamespace ns = stack.get(i);
			if( !isTop && !ns.isGlobal() )
				continue;
			namespacesOnStack.addAll( ns.names() );
			if( ns instanceof JavaNamespace ) {
				JavaNamespace jns = (JavaNamespace)ns;
				Class cls = jns.cls;
				JavaCommand javaCmd = JavaCommand.getJavaCommand(cls,cmdName);
				if( javaCmd != null ) {
					if( !(call instanceof JavaCall && ((JavaCall)call).javaCommand == javaCmd) ) {
						i = iStack(i);
						JavaCall javaCall = new JavaCall(javaCmd,i);
						if( call != null )
							throw conflict(javaCall,call);
						call = javaCall;
						program.addMeaning(javaCmd);
					}
				}
			}
			if( !ns.isTransparent() && isTop )
				isTop = false;
		}
		List<Macro> candidates = macros.get(cmdName);
		if( candidates != null ) {
			for( Macro candidate : candidates ) {
				if( namespacesOnStack.containsAll( candidate.requiredNamespaces ) ) {
					if( call == null ) {
						call = candidate;
					} else {
						if( call.getRequiredNamespaces().containsAll(candidate.getRequiredNamespaces()) ) {
							if( candidate.getRequiredNamespaces().containsAll(call.getRequiredNamespaces()) )
								throw conflict(candidate,call);
							// keep call
						} else if( candidate.getRequiredNamespaces().containsAll(call.getRequiredNamespaces()) ) {
							call = candidate;
						} else {
							throw conflict(candidate,call);
						}
					}
				}
			}
		}
		if( cmdName.equals("overridden") ) {
			if( call != null )
				throw new CompileException(stackTrace,"conflict between ("+call+") and 'overridden' command");
			call = overrides.get(macroScope.macro);
		}
		if( call == null )
			throw new CompileMethodException(stackTrace,"macro or method for '"+cmdName+"' not found in "+accessibleStack()+"  stack = "+stack);
		return call;
	}

	private CompileException conflict(Call call1,Call call2) {
		return new CompileException(stackTrace,"conflict between ("+call1+") and ("+call2+")");
	}

	private static Chunk consolidate(List<Chunk> chunks) {
		List<Chunk> flat = new ArrayList<Chunk>();
		for( Chunk chunk : chunks ) {
			if( chunk instanceof Chunks ) {
				flat.addAll( Arrays.asList(((Chunks)chunk).chunks) );
			} else {
				flat.add(chunk);
			}
		}
		if( flat.size() == 1 )
			return flat.get(0);
		List<Chunk> merged = new ArrayList<Chunk>();
		StringChunk stringChunk = null;
		StringBuilder buf = new StringBuilder();
		for( Chunk chunk : flat ) {
			if( chunk == emptyChunk )
				continue;
			if( chunk instanceof ErrorChunk )
				return chunk;
			if( chunk instanceof StringChunk ) {
				StringChunk sc = (StringChunk)chunk;
				if( stringChunk==null ) {
					stringChunk = sc;
				} else {
					if( buf.length()==0 )
						buf.append( stringChunk.s );
					buf.append( sc.s );
				}
			} else {
				if( buf.length() > 0 ) {
					merged.add( new StringChunk(buf.toString()) );
					buf.setLength(0);
					stringChunk = null;
				} else if( stringChunk != null ) {
					merged.add( stringChunk );
					stringChunk = null;
				}
				merged.add(chunk);
			}
		}
		if( buf.length() > 0 ) {
			merged.add( new StringChunk(buf.toString()) );
		} else if( stringChunk != null ) {
			merged.add( stringChunk );
		}
		return newChunks(merged);
	}

	private static Chunk trimChunk(Chunk chunk) {
		if( chunk instanceof StringChunk ) {
			StringChunk sc = (StringChunk)chunk;
			String s = sc.s.trim();
			if( s.length() == 0 )
				return emptyChunk;
			if( s.length() == sc.s.length() )
				return sc;
			return new StringChunk(s);
		}
		if( chunk instanceof Chunks ) {
			boolean changed = false;
			List<Chunk> chunks = new ArrayList<Chunk>(Arrays.asList(((Chunks)chunk).chunks));
			if( chunks.size() >= 2 ) {
				for( int i=0; i<chunks.size(); ) {
					Chunk firstChunk = chunks.get(i);
					if( firstChunk instanceof StringChunk ) {
						StringChunk sc = (StringChunk)firstChunk;
						String s = sc.s.replaceAll("^\\s+","");
						if( s.length() < sc.s.length() ) {
							changed = true;
							if( s.length() == 0 ) {
								chunks.remove(i);
								continue;
							} else {
								chunks.set(i,new StringChunk(s));
							}
						}
					} else if( !firstChunk.hasOutput() ) {
						i++;
						continue;
					}
					break;
				}
				for( int i = chunks.size() - 1; i>=0; i-- ) {
					Chunk lastChunk = chunks.get(i);
					if( lastChunk instanceof StringChunk ) {
						StringChunk sc = (StringChunk)lastChunk;
						String s = sc.s.replaceAll("\\s+$","");
						if( s.length() < sc.s.length() ) {
							changed = true;
							if( s.length() == 0 ) {
								chunks.remove(i);
								continue;
							} else {
								chunks.set(i,new StringChunk(s));
							}
						}
					} else if( !lastChunk.hasOutput() ) {
						continue;
					}
					break;
				}
				if( changed )
					return newChunks(chunks);
			}
		}
		return chunk;
	}

	private static final Chunk emptyChunk = new Chunk() {

		public void run(IPrintWriter out,RunState runState) {}

		public boolean hasOutput() {
			return false;
		}

		public String toString() {
			return "emptyChunk";
		}

		@Override public Chunk intern() {
			return this;
		}
	};

	private static final class Chunks implements Chunk {
		private Chunk[] chunks;
		private int hash = 0;

		Chunks(List<Chunk> chunks) {
			this.chunks = chunks.toArray(new Chunk[chunks.size()]);
		}

		public void run(IPrintWriter out,RunState runState) {
			for( Chunk chunk : chunks ) {
				chunk.run(out,runState);
			}
		}

		public boolean hasOutput() {
			for( Chunk chunk : chunks ) {
				if( chunk.hasOutput() )
					return true;
			}
			return false;
		}

		public String toString() {
			return "{Chunks: "+chunks+"}";
		}

		@Override public boolean equals(Object obj) {
			if( this==obj )
				return true;
			if( !(obj instanceof Chunks) )
				return false;
			Chunks c = (Chunks)obj;
			final int n = chunks.length;
			if( n != c.chunks.length )
				return false;
			for( int i=0; i<n; i++ ) {
				if( chunks[i] != c.chunks[i] )
					return false;
			}
			return true;
		}

		@Override public int hashCode() {
			if( hash == 0 )
				hash = Arrays.hashCode(chunks);
			return hash;
		}

		@Override public Chunk intern() {
			Chunk[] a = new Chunk[chunks.length];
			for( int i=0; i<a.length; i++ ) {
				a[i] = chunks[i].intern();
			}
			chunks = a;
			return interner.intern(this);
		}
	}

	private static Chunk newChunks(List<Chunk> chunks) {
		switch( chunks.size() ) {
		case 0:
			return emptyChunk;
		case 1:
			return chunks.get(0);
		default:
			return new Chunks(chunks);
		}
	}

	private static class ErrorChunk implements Chunk {
		final CompileException e;

		ErrorChunk(CompileException e) {
			this.e = e;
		}

		public void run(IPrintWriter out,RunState runState) {
			throw new RuntimeException(e);
		}

		public boolean hasOutput() {
			return false;
		}
/*
		@Override public boolean equals(Object obj) {
			throw new RuntimeException("never");
		}

		@Override public int hashCode() {
			throw new RuntimeException("never");
		}
*/
		@Override public Chunk intern() {
			throw new RuntimeException("never");
		}
	}

	private static class DynamicAttr implements Chunk {
		private final Attribute attr;
		private Chunk chunk;

		DynamicAttr(Attribute attr,Chunk chunk) {
			this.attr = attr;
			this.chunk = chunk;
		}

		public void run(IPrintWriter out,RunState runState) {
			String attrValue = new BlockWrapper(chunk,runState).toString();
			if( attrValue != null )
				out.print( attr.toString(attrValue) );
		}

		public boolean hasOutput() {
			return true;
		}

		@Override public boolean equals(Object obj) {
			if( this==obj )
				return true;
			if( !(obj instanceof DynamicAttr) )
				return false;
			DynamicAttr da = (DynamicAttr)obj;
			return da.attr == attr && da.chunk.equals(chunk);
		}

		@Override public int hashCode() {
			return attr.hashCode() * 31 + chunk.hashCode();
		}

		@Override public Chunk intern() {
			chunk = chunk.intern();
			return interner.intern(this);
		}
	}


	private static class VarScope implements Chunk {
		private final Macro macro;
		private Chunk chunk;

		VarScope(Chunk chunk,MacroScope macroScope) {
			this.macro = macroScope.macro;
			this.chunk = chunk;
		}

		public void run(IPrintWriter out,RunState runState) {
			runState.pushVars(macro);
			try {
				chunk.run(out,runState);
			} finally {
				runState.popVars(macro);
			}
		}

		public boolean hasOutput() {
			return chunk.hasOutput();
		}

		public String toString() {
			return "{VarScope}";
		}

		@Override public boolean equals(Object obj) {
			if( this==obj )
				return true;
			if( !(obj instanceof VarScope) )
				return false;
			VarScope v = (VarScope)obj;
			return macro == v.macro && chunk.equals(v.chunk);
		}

		@Override public int hashCode() {
			return chunk.hashCode()*31 + macro.hashCode();
		}

		@Override public Chunk intern() {
			chunk = chunk.intern();
			return interner.intern(this);
		}
	}

	private interface Stringable {
		public String toString(RunState runState);
		public void internChunk();
	}

	private final static class StringableString implements Stringable {
		private final String s;

		StringableString(String s) {
			this.s = s;
		}

		public String toString(RunState runState) {
			return s;
		}

		@Override public boolean equals(Object obj) {
			if( !(obj instanceof StringableString) )
				return false;
			StringableString ss = (StringableString)obj;
			return ObjectUtils.equals(ss.s,s);
		}

		@Override public int hashCode() {
			int h = getClass().hashCode();
			if( s != null )
				h += s.hashCode();
			return h;
		}

		public void internChunk() {}
	}

	private final static class StringableChunk implements Stringable {
		private Chunk chunk;

		StringableChunk(Chunk chunk) {
			this.chunk = chunk;
		}

		public String toString(RunState runState) {
			return new BlockWrapper(chunk,runState).toString();
		}

		@Override public boolean equals(Object obj) {
			if( !(obj instanceof StringableChunk) )
				return false;
			StringableChunk sc = (StringableChunk)obj;
			return sc.chunk == chunk;
		}

		@Override public int hashCode() {
			return chunk.hashCode() + getClass().hashCode();
		}

		public void internChunk() {
			chunk = chunk.intern();
		}
	}

	private static Stringable getStringable(String name,Map<String,String> staticArgs,Map<String,Chunk> dynamicArgs) {
		Chunk chunk = dynamicArgs.get(name);
		if( chunk != null )
			return new StringableChunk(chunk);
		String s = staticArgs.get(name);
		return new StringableString(s);
	}

	private static final class GetVar implements Chunk {
		private final Macro macro;
		private final Stringable name;

		GetVar(MacroScope macroScope,Map<String,String> staticArgs,Map<String,Chunk> dynamicArgs) {
			macroScope.hasVars = true;
			this.macro = macroScope.macro;
			this.name = getStringable("name",staticArgs,dynamicArgs);
		}

		public void run(IPrintWriter out,RunState runState) {
			String nameS = name.toString(runState);
			String valueS = runState.getVars(macro).get(nameS);
			out.print(valueS);
		}

		public boolean hasOutput() {
			return true;
		}

		public String toString() {
			return "{GetVar}";
		}

		@Override public boolean equals(Object obj) {
			if( !(obj instanceof GetVar) )
				return false;
			GetVar v = (GetVar)obj;
			return v.macro == macro && v.name.equals(name);
		}

		@Override public int hashCode() {
			int h = macro.hashCode();
			h = h * 31 + name.hashCode();
			return h;
		}

		@Override public Chunk intern() {
			name.internChunk();
			return interner.intern(this);
		}
	}

	private static final class SetVar implements Chunk {
		private final Macro macro;
		private final Stringable name;
		private final Stringable value;

		SetVar(MacroScope macroScope,Map<String,String> staticArgs,Map<String,Chunk> dynamicArgs) {
			macroScope.hasVars = true;
			this.macro = macroScope.macro;
			this.name = getStringable("name",staticArgs,dynamicArgs);
			this.value = getStringable("value",staticArgs,dynamicArgs);
		}

		public void run(IPrintWriter out,RunState runState) {
			String nameS = name.toString(runState);
			String valueS = value.toString(runState);
			runState.getVars(macro).put(nameS,valueS);
		}

		public boolean hasOutput() {
			return false;
		}

		public String toString() {
			return "{SetVar}";
		}

		@Override public boolean equals(Object obj) {
			if( !(obj instanceof SetVar) )
				return false;
			SetVar v = (SetVar)obj;
			return v.macro == macro && v.name.equals(name) && v.value.equals(value);
		}

		@Override public int hashCode() {
			int h = macro.hashCode();
			h = h * 31 + name.hashCode();
			h = h * 31 + value.hashCode();
			return h;
		}

		@Override public Chunk intern() {
			name.internChunk();
			value.internChunk();
			return interner.intern(this);
		}
	}


	private static final Set<JavaCommand> badMethods = new CopyOnWriteArraySet<JavaCommand>();
	private static final Map<JavaCommand,Set<String>> badParams = new ConcurrentHashMap<JavaCommand,Set<String>>();

	private static class CompileTimeIntepreterException extends RuntimeException {}

	private static class CompileTimeArgException extends RuntimeException {
		final String argName;

		CompileTimeArgException(String argName) {
			this.argName = argName;
		}
	}

	private static class CompileTimeScopedException extends RuntimeException {

		CompileTimeScopedException(CompileTimeIntepreterException e) {
			super(e);
		}
	}

	private static class ChunkWrapper {
		final String argName;
		final Chunk chunk;

		ChunkWrapper(String argName,Chunk chunk) {
			this.argName = argName;
			this.chunk = chunk;
		}

		public String toString() {
			throw new CompileTimeArgException(argName);
		}
	}

	static abstract class Traceable {
		StackTrace stackTrace = new StackTrace();

		Traceable(Compiler compiler) {
			compiler.traceable = this;
			compiler.traceables.add(this);
		}

		private void cleanUp() {
			Collections.reverse(stackTrace);
			stackTrace = stackTrace.intern();
		}
	}

	static boolean nestedEquals(Map<String,Chunk> map1,Map<String,Chunk> map2) {
		if( map1.size() != map2.size() )
			return false;
		for( Map.Entry<String,Chunk> entry : map1.entrySet() ) {
			if( entry.getValue() != map2.get(entry.getKey()) )
				return false;
		}
		return true;
	}

	private static class Block extends Traceable implements Chunk {
		final JavaCall method;
		final Map<Class,Integer> inStack;
		private final Map<String,String> staticArgs;
		private Map<String,Chunk> dynamicArgs;
		private final boolean hasOutput;
		private int hash = 0;

		Block(Compiler compiler,JavaCall method,Map<Class,Integer> inStack,Map<String,String> staticArgs,Map<String,Chunk> dynamicArgs) {
			this(compiler,method,inStack,staticArgs,dynamicArgs,Collections.<String>emptySet());
		}

		Block(Compiler compiler,JavaCall method,Map<Class,Integer> inStack,Map<String,String> staticArgs,Map<String,Chunk> dynamicArgs,Set<String> scopedArgs) {
			super(compiler);
			this.method = method;
			this.inStack = CollectionUtils.optimizeMap(inStack);
			this.staticArgs = CollectionUtils.optimizeMap(staticArgs);
			this.dynamicArgs = CollectionUtils.optimizeMap(dynamicArgs);
			Set<String> outputtedParameters = method.cmdSpec().outputtedParameters;
			if( outputtedParameters == null ) {
				this.hasOutput = true;
			} else {
				boolean hasOutput = false;
				Set<String> staticArgNames = staticArgs.keySet();
				for( String s : outputtedParameters ) {
					if( staticArgNames.contains(s) ) {
						hasOutput = true;
						break;
					}
					Chunk chunk = dynamicArgs.get(s);
					if( chunk == null ) {
						if( scopedArgs.contains(s) ) {
							hasOutput = true;
							break;
						}
					} else if( chunk.hasOutput() ) {
						hasOutput = true;
						break;
					}
				}
				this.hasOutput = hasOutput;
			}
		}

		private void invoke(RunState runState,IPrintWriter out,InterpreterImpl interp)
			throws IllegalAccessException, InvocationTargetException
		{
			try {
				method.invoke(runState,out,interp);
			} finally {
				interp.close();
			}
		}

		private Chunk prerun(RunState runState)
			throws CompileException
		{
			try {
				JavaCommand javaCommand = method.javaCommand;
				if( !javaCommand.isStatic || badMethods.contains(javaCommand) )
					return null;
				{
					Set<String> params = badParams.get(javaCommand);
					if( params != null ) {
						Set<String> chunkSet = dynamicArgs.keySet();
						for( String param : params ) {
							if( chunkSet.contains(param) )
								return null;
						}
					}
				}
				CompilerPrintWriter out = new CompilerPrintWriter(new CharArrayWriter());
//				RunState runState = new CompileTimeRunState();
				try {
					Map<String,Object> args = new HashMap<String,Object>(staticArgs);
					for( Map.Entry<String,Chunk> entry : dynamicArgs.entrySet() ) {
						String argName = entry.getKey();
						Chunk argValue = entry.getValue();
						args.put( argName, new ChunkWrapper(argName,argValue) );
					}
					invoke(runState,out,newInterpreter(runState,args));
					out.close();
					return Compiler.consolidate(out.chunks);
				} catch(InvocationTargetException e) {
					Throwable cause = e.getCause();
					if( cause instanceof CompileTimeIntepreterException ) {
						badMethods.add(javaCommand);
						logger.debug("bad method: "+javaCommand.method,cause);
					} else if( cause instanceof CompileTimeArgException ) {
						CompileTimeArgException ex = (CompileTimeArgException)cause;
						Set<String> params = badParams.get(javaCommand);
						if( params == null ) {
							params = new CopyOnWriteArraySet<String>();
							badParams.put(javaCommand,params);
						}
						params.add(ex.argName);
//						logger.debug("bad argument '"+ex.argName+"' in "+javaCommand.method);
					} else if( cause instanceof CompileTimeScopedException ) {
						logger.debug("ignoring: "+javaCommand.method,cause);
					} else {
						throw e;
					}
				}
				return null;
			} catch(InvocationTargetException e) {
				throw compileFix(e);
			} catch(IllegalAccessException e) {
				throw new TemplateRuntimeException(e);
			}
		}

		public void run(IPrintWriter out,RunState runState) {
			Stack<StackTrace> stack = StackTrace.stack();
			stack.push(stackTrace);
			try {
				if( dynamicArgs.isEmpty() ) {
					invoke(runState,out,newInterpreter(runState,staticArgs));
				} else {
					Map<String,Object> args = new HashMap<String,Object>(staticArgs);
					for( Map.Entry<String,Chunk> entry : dynamicArgs.entrySet() ) {
						args.put( entry.getKey(), new BlockWrapper(entry.getValue(),runState) );
					}
					invoke(runState,out,newInterpreter(runState,args));
				}
			} catch(IllegalAccessException e) {
				throw new TemplateRuntimeException(e);
			} catch(InvocationTargetException e) {
				throw interpFix(e);
			} finally {
				stack.pop();
			}
		}

		public boolean hasOutput() {
			return hasOutput;
		}

		InterpreterImpl newInterpreter(RunState runState,Map<String,?> args) {
			return new InterpreterImpl(method.cmdSpec(),runState,inStack,args);
		}

		public String toString() {
			return "{Block: "+stackTrace.peek()+"}";
		}

		@Override public boolean equals(Object obj) {
			if( this==obj )
				return true;
			if( !(obj instanceof Block) )
				return false;
			return equals((Block)obj);
		}

		boolean equals(Block block) {
			return block.method.equals(method)
				&& block.inStack.equals(inStack)
				&& block.staticArgs.equals(staticArgs)
				&& nestedEquals(block.dynamicArgs,dynamicArgs)
				&& block.getClass().equals(getClass())
			;
		}

		@Override public int hashCode() {
			if( hash == 0 )
				hash = calcHash();
			return hash;
		}

		int calcHash() {
			int h = method.hashCode();
			h = h * 31 + inStack.hashCode();
			h = h * 31 + staticArgs.hashCode();
			h = h * 31 + dynamicArgs.hashCode();
			return h;
		}

		void internMembers() {
			Map<String,Chunk> m = new HashMap<String,Chunk>();
			for( Map.Entry<String,Chunk> entry : dynamicArgs.entrySet() ) {
				m.put(entry.getKey(),entry.getValue().intern());
			}
			dynamicArgs = CollectionUtils.optimizeMap(m);
		}

		@Override public final Chunk intern() {
			internMembers();
			return interner.intern(this);
		}
	}

	private static final class Scoped extends Block {
		private Map<String,Chunk> scopedArgs;

		Scoped(Compiler compiler,JavaCall method,Map<Class,Integer> inStack,Map<String,String> staticArgs,Map<String,Chunk> dynamicArgs,Map<String,Chunk> scopedArgs) {
			super(compiler,method,inStack,staticArgs,dynamicArgs,scopedArgs.keySet());
			this.scopedArgs = CollectionUtils.optimizeMap(scopedArgs);
		}

		InterpreterImpl newInterpreter(RunState runState,Map<String,?> args) {
			return new ScopedInterpreterImpl<Object>(method.cmdSpec(),runState,inStack,args,scopedArgs);
		}

		boolean equals(Block block) {
			if( !super.equals(block) )
				return false;
			Scoped scoped = (Scoped)block;
			return nestedEquals(scoped.scopedArgs,scopedArgs);
		}

		int calcHash() {
			return super.calcHash() * 31 + scopedArgs.hashCode();
		}

		void internMembers() {
			super.internMembers();
			Map<String,Chunk> m = new HashMap<String,Chunk>();
			for( Map.Entry<String,Chunk> entry : scopedArgs.entrySet() ) {
				m.put(entry.getKey(),entry.getValue().intern());
			}
			scopedArgs = CollectionUtils.optimizeMap(m);
		}
	}

	private static final class Subroutine extends Traceable implements Chunk {
		private final Map<String,Stringable> argMap;
		private final int[] aiStack;
		private final GenericNamespace[] base;
		private final String commandName;
		private Template template = null;
		private int hash = 0;

		Subroutine(Compiler compiler,Map<String,Stringable> argMap,int[] aiStack,GenericNamespace[] base,String commandName) {
			super(compiler);
			this.argMap = argMap;
			this.aiStack = aiStack;
			this.base = base;
			this.commandName = commandName;
		}

		public void run(IPrintWriter out,RunState runState) {
			Stack<StackTrace> stackTraces = StackTrace.stack();
			stackTraces.push(stackTrace);
			try {
				synchronized(this) {
					if( template == null ) {
						try {
							template = runState.template().program().getTemplate(commandName,base);
						} catch(CompileException e) {
							throw new TemplateRuntimeException(e);
						}
						if( template == null )
							throw new TemplateRuntimeException("couldn't find: "+commandName);
					}
				}
				Map<String,Object> args = new HashMap<String,Object>();
				for( Map.Entry<String,Stringable> entry : argMap.entrySet() ) {
					String value = entry.getValue().toString(runState);
					if( value != null )
						args.put( entry.getKey(), value );
				}
				Object[] stack = new Object[aiStack.length];
				for( int i=0; i<aiStack.length; i++ ) {
					stack[i] = runState.getFromStack(aiStack[i]);
				}
				template.run( runState.callDepth()+1, runState.getEncoder(), out, args, stack );
			} finally {
				stackTraces.pop();
			}
		}

		public boolean hasOutput() {
			return true;
		}

		public String toString() {
			return "{Subroutine: "+commandName+"}";
		}
/*
		@Override public boolean equals(Object obj) {
			if( this==obj )
				return true;
			if( !(obj instanceof Subroutine) )
				return false;
			Subroutine sub = (Subroutine)obj;
			return sub.commandName.equals(commandName)
				&& sub.argMap.equals(argMap)
				&& Arrays.equals(sub.aiStack,aiStack)
				&& Arrays.equals(sub.base,base)
			;
		}

		@Override public int hashCode() {
			if( hash == 0 ) {
				int h = commandName.hashCode();
				h = h * 31 + argMap.hashCode();
				h = h * 31 + Arrays.hashCode(aiStack);
				h = h * 31 + Arrays.hashCode(base);
				hash = h;
			}
			return hash;
		}
*/
		@Override public final Chunk intern() {
			for( Stringable s : argMap.values() ) {
				s.internChunk();
			}
			return interner.intern(this);
		}
	}

	private static CompileException compileFix(InvocationTargetException e) {
		Throwable t = e.getCause();
		if( t instanceof Error )
			throw (Error)t;
		if( t instanceof TemplateRuntimeException )
			throw (TemplateRuntimeException)t;
		if( t instanceof CompileException )
			return (CompileException)t;
		throw new TemplateRuntimeException((Exception)t);
	}


	// from old CompilerDataImpl
/*
	private Chunk runHandler(Block block)
		throws CompileException
	{
		JavaCommand javaCommand = block.method.javaCommand;
		if( !javaCommand.isStatic || badMethods.contains(javaCommand) )
			return null;
		CompilerPrintWriter writer = new CompilerPrintWriter(new CharArrayWriter());
		RunState runState = new CompileTimeRunState();
		try {
			block.run(writer,runState);
			writer.close();
			return Compiler.consolidate(writer.chunks);
		} catch(CompileTimeIntepreterException e) {
			badMethods.add(javaCommand);
			logger.info("bad method: "+javaCommand.method);
		} catch(CompileTimeArgException e) {
//e.printStackTrace();
			// nothing
		}
		return null;
	}
*/
	private static class CompilerPrintWriter extends TemplatePrintWriter {
		private final CharArrayWriter sw;
		private boolean isNull = false;
		final List<Chunk> chunks = new ArrayList<Chunk>();

		CompilerPrintWriter(CharArrayWriter sw) {
			super(sw);
			this.sw = sw;
		}

		public void print(Object obj) {
			if( obj == null )
//				return;
				throw new RuntimeException("why");
			if( obj instanceof ChunkWrapper ) {
				addStringChunk();
				chunks.add( ((ChunkWrapper)obj).chunk );
			} else if( obj instanceof BlockWrapper ) {  // should only happen for scoped args
				try {
					super.print(obj);
				} catch(CompileTimeIntepreterException e) {
					throw new CompileTimeScopedException(e);
				}
			} else {
				super.print(obj);
			}
		}

		public void print(String s) {
			if( s == null && sw.size() == 0 && chunks.isEmpty() && !isNull ) {
				isNull = true;
				return;
			}
			super.print(s);
		}

		public void close() {
			super.close();
			addStringChunk();
			if( isNull ) {
				if( !chunks.isEmpty() )
					throw new NullPointerException("null written to stream");
				chunks.add(Chunk.NULL);
			}
		}

		private void addStringChunk() {
			if( sw.size() > 0 ) {
				chunks.add( new StringChunk(sw.toString()) );
				sw.reset();
			}
		}
	}

	private final class CompileTimeRunState implements RunState {
		private final Stack<Object> stack = new ArrayStack<Object>();

		CompileTimeRunState() {
			int n = Compiler.this.stack.size();
			while( n-- > 0 ) {
				stack.push(null);
			}
		}

		public Template template() {
			throw new CompileTimeIntepreterException();
		}

		public Program program() {
			return program;
		}

		public int callDepth() {
			throw new CompileTimeIntepreterException();
		}

		public void putArg(String name,String value) {
			throw new CompileTimeIntepreterException();
		}

		public String getArg(String name) {
			throw new CompileTimeIntepreterException();
		}

		public Object getNamespace(String key) {
			throw new CompileTimeIntepreterException();
		}

		public String saveNamespace(Object namespace) {
			throw new CompileTimeIntepreterException();
		}

		public Object getFromStack(int i) {
			if( i < 0 )
				i = stack.size() + i;
			Object obj = stack.get(i);
			if( obj == null )
				throw new CompileTimeIntepreterException();
			return obj;
		}

		public int push(Object scope) {
			return RunStateImpl.push(stack,program.extensionMap(),scope);
		}

		public void pop(int n) {
			RunStateImpl.pop(stack,n);
		}

		public boolean hasNamespace(String namespace) {
			return Compiler.this.hasNamespace(namespace);
		}

		public boolean isInCommandStack(String commandName) {
			for( StackTraceElement stackElem : stackTrace ) {
				if( commandName.equals(stackElem.commandName()) )
					return true;
			}
			return false;
		}

		public Encoder getEncoder() {
			return Encoder.TEXT;
		}

		public void setEncoder(Encoder encoder) {
			if( encoder != Encoder.TEXT )
				throw new CompileTimeIntepreterException();
		}

		public Map<String,String> getVars(Macro macro) {
			throw new CompileTimeIntepreterException();
		}

		public void pushVars(Macro macro) {
			throw new CompileTimeIntepreterException();
		}

		public void popVars(Macro macro) {
			throw new CompileTimeIntepreterException();
		}

	}

	static RuntimeException interpFix(InvocationTargetException e) {
		Throwable t = e.getCause();
		if( t instanceof Error )
			throw (Error)t;
		if( t instanceof TemplateRuntimeException )
			return (TemplateRuntimeException)t;
		if( t instanceof ExitException )
			return (ExitException)t;
		return new TemplateRuntimeException((Exception)t);
	}

}