package com.laytonsmith.core;
import com.laytonsmith.core.compiler.FileOptions;
import com.laytonsmith.core.constructs.CFunction;
import com.laytonsmith.core.constructs.CString;
import com.laytonsmith.core.constructs.Construct;
import com.laytonsmith.core.constructs.IVariable;
import com.laytonsmith.core.constructs.Target;
import com.laytonsmith.core.exceptions.ConfigCompileException;
import com.laytonsmith.core.functions.Function;
import com.laytonsmith.core.functions.FunctionBase;
import com.laytonsmith.core.functions.FunctionList;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
/**
* A parse tree wraps a generic tree node, but provides functions that are commonly used to discover
* things about a particular section of code.
*
*/
public class ParseTree implements Cloneable{
private enum CacheTypes{
IS_SYNC, IS_ASYNC, FUNCTIONS
}
/**
* Since for the most part, this is a wrapper class, we want instantiations
* to be cheap, both time and memory. However we also want to be able to cache certain information,
* since many of our operations are fairly expensive,
* so we also want to maintain a cache. But we ALSO don't want
* to have a memory leak by simply having tons of cached references. So, we
* store a private cache of weak references to "this" instance.
*/
private static Map<ParseTree, Map<CacheTypes, Object>> cache
= new WeakHashMap<ParseTree, Map<CacheTypes, Object>>();
private static boolean isCached(ParseTree tree, CacheTypes type){
if(!cache.containsKey(tree)){
return false;
} else {
return cache.get(tree).containsKey(type);
}
}
/**
* Returns the value from the cache. This will throw an Error if no cached
* value exists, so you must ALWAYS call isCached first.
* @param tree
* @param type
* @return
*/
private static Object getCache(ParseTree tree, CacheTypes type){
if(!isCached(tree, type)){
throw new Error("It is an error to call getCache on an object that does not already have a cached value");
}
return cache.get(tree).get(type);
}
private static void setCache(ParseTree tree, CacheTypes type, Object value){
if(!cache.containsKey(tree)){
cache.put(tree, new EnumMap<CacheTypes, Object>(CacheTypes.class));
}
cache.get(tree).put(type, value);
}
private static void clearCache(ParseTree tree){
cache.remove(tree);
}
private Construct data = null;
private boolean isOptimized = false;
private final FileOptions fileOptions;
private List<ParseTree> children = null;
private boolean hasBeenMadeStatic = false;
/**
* Creates a new empty tree node
*/
public ParseTree(FileOptions options){
children = new ArrayList<ParseTree>();
this.fileOptions = options;
}
/**
* Creates a new tree node, with this construct as the data
* @param construct
*/
public ParseTree(Construct construct, FileOptions options){
this(options);
setData(construct);
}
public FileOptions getFileOptions(){
return fileOptions;
}
public void setData(Construct data) {
this.data = data;
}
public void setOptimized(boolean optimized){
isOptimized = optimized;
}
public boolean isOptimized(){
return isOptimized;
}
public boolean hasBeenMadeStatic() {
return hasBeenMadeStatic;
}
public void hasBeenMadeStatic(boolean state){
hasBeenMadeStatic = state;
}
/**
* Returns a flat list of all node data. This can be used when an entire tree
* needs to be scoured for information, regardless of visitation order.
* @return
*/
public List<Construct> getAllData(){
List<Construct> list = new ArrayList<Construct>();
list.add(getData());
for(ParseTree node : getChildren()){
list.addAll(node.getAllData());
}
return list;
}
/**
* Returns a list of direct children
* @return
*/
public List<ParseTree> getChildren(){
return children;
}
/**
* Gets the child at the index specified.
* @param index
* @throws IndexOutOfBoundsException if the index overflows
* @return
*/
public ParseTree getChildAt(int index){
return children.get(index);
}
/**
* Returns the data in this node
* @return
*/
public Construct getData(){
return data;
}
/**
* Returns true if this node has children
* @return
*/
public boolean hasChildren(){
return !children.isEmpty();
}
public void setChildren(List<ParseTree> children){
this.children = children;
}
/**
* Adds a child
* @param node
*/
public void addChild(ParseTree node){
children.add(node);
}
/**
* Adds a child at the specified index
* @param index
* @param node
*/
public void addChildAt(int index, ParseTree node){
children.add(index, node);
}
/**
* Returns the number of children this node has
* @return
*/
public int numberOfChildren(){
return children.size();
}
/**
* Removes a child at the specified index
* @param index
*/
public void removeChildAt(int index){
children.remove(index);
}
/**
* Removes all children from this node
*/
public void removeChildren(){
children.clear();
}
/**
* A value is considered "const" if it doesn't have any children. An array
* is considered a const, though the array function is not. That is to say,
* the value is determined for sure, despite the rest of the code, this will
* always return the same value. Most constructs are const.
* @return
*/
public boolean isConst(){
return !data.isDynamic();
}
/**
* Returns the opposite of isConst().
* @return
*/
public boolean isDynamic(){
return data.isDynamic();
}
//TODO: None of this will work until we deeply consider procs, which can't happen yet.
// /**
// * If ANY data node REQUIRES this to be async, this will return true. If
// * NONE of the data nodes REQUIRE this to be async, or if NONE of them care,
// * it returns false.
// * @return
// */
// public boolean isAsync(){
// if(isCached(this, CacheTypes.IS_ASYNC)){
// return (Boolean)getCache(this, CacheTypes.IS_ASYNC);
// } else {
// boolean ret = false;
// for(Function ff : getFunctions()){
// Boolean runAsync = ff.runAsync();
// if(runAsync != null && runAsync == true){
// //We're done here. It's definitely async only,
// //so we can stop looking.
// ret = true;
// }
// }
// setCache(this, CacheTypes.IS_ASYNC, ret);
// return ret;
// }
// }
//
// /**
// * If ANY data node REQUIRES this to be sync, this will return true. If
// * NONE of the data nodes REQUIRE this to be sync, or if NONE of them care,
// * it returns false.
// * @return
// */
// public boolean isSync(){
// if(isCached(this, CacheTypes.IS_SYNC)){
// return (Boolean)getCache(this, CacheTypes.IS_SYNC);
// } else {
// boolean ret = false;
// for(Function ff : getFunctions()){
// Boolean runAsync = ff.runAsync();
// if(runAsync != null && runAsync == false){
// //We're done here. It's definitely sync only,
// //so we can stop looking.
// ret = true;
// }
// }
// setCache(this, CacheTypes.IS_SYNC, ret);
// return ret;
// }
// }
/**
* Returns a list of all functions contained in this parse tree.
* @return
*/
public List<Function> getFunctions(){
if(isCached(this, CacheTypes.FUNCTIONS)){
return new ArrayList<Function>((List<Function>)getCache(this, CacheTypes.FUNCTIONS));
} else {
List<Function> functions = new ArrayList<Function>();
List<Construct> allChildren = getAllData();
loop: for(Construct c : allChildren){
if(c instanceof CFunction){
try {
FunctionBase f = FunctionList.getFunction(c);
if(f instanceof Function){
Function ff = (Function)f;
functions.add(ff);
}
} catch (ConfigCompileException ex) {
throw new Error(ex);
}
}
}
setCache(this, CacheTypes.FUNCTIONS, functions);
return new ArrayList<Function>(functions);
}
}
@Override
public ParseTree clone() throws CloneNotSupportedException {
ParseTree clone = (ParseTree)super.clone();
clone.data = data.clone();
clone.children = new ArrayList<ParseTree>(this.children);
return clone;
}
@Override
public String toString(){
return data.toString();
}
public String toStringVerbose(){
StringBuilder stringRepresentation = new StringBuilder();
if(data instanceof CFunction){
stringRepresentation.append(data.toString());
stringRepresentation.append("(");
boolean first = true;
for(ParseTree child : children){
if(!first){
stringRepresentation.append(", ");
}
first = false;
stringRepresentation.append(child.toStringVerbose());
}
stringRepresentation.append(")");
} else if (data instanceof CString) {
// Convert: \ -> \\ and ' -> \'
stringRepresentation.append("'").append(data.val().replaceAll("\t", "\\t").replaceAll("\n", "\\n").replace("\\", "\\\\").replace("'", "\\'")).append("'");
} else if(data instanceof IVariable){
stringRepresentation.append(((IVariable)data).getVariableName());
} else {
stringRepresentation.append(data.val());
}
return stringRepresentation.toString();
}
public Target getTarget(){
if(data == null){
return Target.UNKNOWN;
} else {
return data.getTarget();
}
}
}