/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * DebugSupport.java * Created: Mar 14, 2006 * By: Bo Ilic */ package org.openquark.cal.runtime; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import org.openquark.cal.util.ArrayStack; /** * Contains various helper methods implementing machine-independent debugging support. * In particular there are methods for displaying a textual debug string from a CalValue, as well as for * calculating various useful statistics based on the CalValue. These methods all have in common that they * don't modify the CalValue in any way, and thus are useful, when combined with tracing (such as with the * Debug.trace function), for viewing the actual state of values in CAL as execution is occurring, without altering * the execution's reduction sequence. * * @author Bo Ilic */ public final class DebugSupport { /** * Immutable value class holding statistical information gathered from a CalValue * * The statistics/information gathered by this class are for debugging purposes only and should * not be relied upon in production code. They are subject to change and their precise value will * in general depend on the particular machine type (lecc or g) used. * * @author Bo Ilic */ public static final class InternalValueStats { /** * The number of distinct nodes in the graph of nodes represented by a given CalValue * The distinct node count provides a measure of how much space the CalValue is taking. Nodes which * are shared and appear multiple times within the graph are only counted once. */ private final int nDistinctNodes; /** * The number of distinct indirection nodes in the graph of nodes represented by a given CalValue. * The distinct indirection node count provides a measure of how much uncompacted or wasted space the * CalValue is taking. Indirection nodes which are shared and appear multiple times within the graph * are only counted once. */ private final int nDistinctIndirectionNodes; /** * The number of nodes in the graph a given CalValue that are shared within the graph itself. */ private final int nSharedNodes; private InternalValueStats(int nDistinctNodes, int nDistinctIndirectionNodes, int nSharedNodes) { this.nDistinctNodes = nDistinctNodes; this.nDistinctIndirectionNodes = nDistinctIndirectionNodes; this.nSharedNodes = nSharedNodes; } /** * This value is for debugging purposes only and is subject to change. * @return The number of distinct nodes in the graph of nodes represented by a given CalValue. * The distinct node count provides a measure of how much space the CalValue is taking. Nodes which * are shared and appear multiple times within the graph are only counted once. */ public int getNDistinctNodes() { return nDistinctNodes; } /** * This value is for debugging purposes only and is subject to change. * @return the number of distinct indirection nodes in the graph of nodes represented by a given CalValue. * The distinct indirection node count provides a measure of how much uncompacted or wasted space the * CalValue is taking. Indirection nodes which are shared and appear multiple times within the graph * are only counted once. */ public int getNDistinctIndirectionNodes() { return nDistinctIndirectionNodes; } /** * This value is for debugging purposes only and is subject to change. * @return the number of nodes in the graph a given CalValue that are shared within the graph itself. */ public int getNSharedNodes() { return nSharedNodes; } /** * Creates a DebugNodeInfo object which gathers statistics from the argument CalValue. * The CalValue node is not modified in any way by calling this method. * * @param calValue * @return a DebugNodeInfo object gathering statistics based from the given argument CalValue. */ public static final InternalValueStats make(CalValue calValue) { if (calValue == null) { throw new NullPointerException(); } //do a stackless preorder traversal of the CalValue node graph gathering the statistics as we go along. //The stackless traversal is so that we are guaranteed not to exhaust the rather limited default Java stack //while traversing a deeply recursive CAL value. //Note: we only traverse nodes not already previously encountered. We could have previously encountered a node //due to sharing in the value graph. This can happen due to a number of reasons: //a) a let variable or argument variable being reused with an expression //b) caching of certain simple values e.g. the lecc machine caches commonly used boxed int values (i.e. RTData.CAL_Int // objects are shared if the underlying int value falls in the caching range). //c) recursively defined variables creating cyclic value graphs Set<CalValue> distinctNodeSet = new HashSet<CalValue>(); //set of CalValue objects int nDistinctIndirectionNodes = 0; int nSharedNodes = 0; //the stack will hold elements of type CalValue ArrayStack<CalValue> stack = ArrayStack.make(); CalValue node = unwrapOpaque(calValue); stack.push(node); distinctNodeSet.add(node); if (node.debug_isIndirectionNode()) { ++nDistinctIndirectionNodes; } while (!stack.isEmpty()) { node = stack.pop(); final int nChildren = node.debug_getNChildren(); for (int i = nChildren - 1; i >= 0; --i) { CalValue childNode = node.debug_getChild(i); if (childNode != null) { if (distinctNodeSet.add(childNode)) { stack.push(childNode); if (childNode.debug_isIndirectionNode()) { ++nDistinctIndirectionNodes; } } else { ++nSharedNodes; } } } } return new InternalValueStats(distinctNodeSet.size(), nDistinctIndirectionNodes, nSharedNodes); } @Override public String toString() { StringBuilder sb = new StringBuilder("(nDistinctNodes = "); sb.append(nDistinctNodes); sb.append(", nDistinctIndirectionNodes = "); sb.append(nDistinctIndirectionNodes); sb.append(", nSharedNodes = "); sb.append(nSharedNodes); sb.append(')'); return sb.toString(); } } private DebugSupport() { //make non-instantiable. } /** * Returns a representation of the argument CalValue useful for debugging purposes. This representation * is subject to change and should not be relied upon in production code. Does not do any * evaluation or modify the CalValue in any way. * * <P>showInternal does not traverse the CalValue graph by using the * Java call stack, and hence will not throw a java.lang.StackOverflowError. For example, * it can handle displaying evaluated CAL lists that are thousands of elements long. * Also, showInternal will successfully display cyclic value graphs. * * <P>showInternal will show some of the sharing of graph nodes in the graph structure of the CalValue. * If a node having one or more children is shared, its first appearance will be marked * e.g. <@nodeNumber = nodeText>, and subsequent appearances will just display as <@nodeNumber>. * Nodes having zero children that are shared (such as function nodes or simple value nodes) are not * shown as being shared. This makes the output easier to read while showing the most important sharing. * To see more of the graph structure, use showInternalGraph. * * <P>Any exceptions (subclasses of Throwable) are caught and rethrown as a java.lang.RuntimeException with * cause set to the caught exception, and a message displaying a partial showInternal result string * if one is available. The main cause of exceptions will be an exception in Object.toString() on one of the * foreign objects held onto by the CalValue graph. * * @param calValue CalValue whose textual representation is to be displayed. Cannot be null. * @return a textual representation of the CalValue for debugging purposes. */ public static final String showInternal(CalValue calValue) { return showInternalGraph(new CalValue[]{calValue}, false, false)[0]; } /** * Returns representations of the argument CalValue elements useful for debugging purposes. This representation * is subject to change and should not be relied upon in production code. Does not do any * evaluation or modify the CalValue in any way. * * <P>showInternal does not traverse the CalValue graph by using the * Java call stack, and hence will not throw a java.lang.StackOverflowError. For example, * it can handle displaying evaluated CAL lists that are thousands of elements long. * Also, showInternal will successfully display cyclic value graphs. * * <P>showInternal will show some of the sharing of graph nodes across the graph structures of the CalValue instances. * If a node having one or more children is shared, its first appearance will be marked * e.g. <@nodeNumber = nodeText>, and subsequent appearances will just display as <@nodeNumber>. * Nodes having zero children that are shared (such as function nodes or simple value nodes) are not * shown as being shared. This makes the output easier to read while showing the most important sharing. * To see more of the graph structure, use showInternalGraph. * * <P>Any exceptions (subclasses of Throwable) are caught and rethrown as a java.lang.RuntimeException with * cause set to the caught exception, and a message displaying a partial showInternal result string * if one is available. The main cause of exceptions will be an exception in Object.toString() on one of the * foreign objects held onto by the CalValue graph. * * @param calValues array of CalValue whose textual representations are to be displayed. Cannot be null. * @return a textual representation of the each of the CalValue for debugging purposes. */ public static final String[] showInternal(CalValue[] calValues) { return showInternalGraph(calValues, false, false); } /** * Returns a representation of the CalValue argument useful for debugging purposes. This representation * is subject to change and should not be relied upon in production code. Does not do any * evaluation or modify the CalValue in any way. * * <P>showInternalGraph does not traverse the CalValue graph by using the * Java call stack, and hence will not throw a java.lang.StackOverflowError. For example, * it can handle displaying evaluated CAL lists that are thousands of elements long. * Also, showInternalGraph will successfully display cyclic value graphs. * * <P>showInternalGraph attempts to show more of the graph structure of the CalValue. If a node is shared, * its first appearance will be marked e.g. <@nodeNumber = nodeText>, and subsequent appearances will just * display as <@nodeNumber>. Indirections are shown using an asterisk *. * To see less of the graph structure, and potentially a more readable output, use showInternal. * * <P>Any exceptions (subclasses of Throwable) are caught and rethrown as a java.lang.RuntimeException with * cause set to the caught exception, and a message displaying a partial showInternalGraph result string * if one is available. The main cause of exceptions will be an exception in Object.toString() on one of the * foreign objects held onto by the CalValue graph. * * @param calValue * @return a textual representation of the CalValue argument for debugging purposes. */ public static final String showInternalGraph(CalValue calValue) { return showInternalGraph(new CalValue[]{calValue}, true, true)[0]; } /** * @param calValue * @return if the opaque value held onto by a CAL_Opaque or NValObject is in fact an CalValue, then return that * CalValue, otherwise just return value. */ private static CalValue unwrapOpaque(CalValue calValue) { //todoBI this is a hack that can be taken out when we can find a way so that Debug.distinctNodeCount //does not wrap its CalValue in a CALOpaque. return calValue.internalUnwrapOpaque(); } /** * Returns a representation of the argument CalValue useful for debugging purposes. This representation * is subject to change and should not be relied upon in production code. Does not do any * evaluation or modify the CalValue in any way. * * <P>This method is mainly for illustrating the meaning of the contract that the abstract methods * CalValue.debug_getNChildren, CalValue.debug_getChild, CalValue.debug_getNodeStartText, * CalValue.debug_getNodeEndText and CalValue.debugGetChildPrefixText * must adhere to in their implementations. Real code should call DebugSupport.showInternal or showInternalGraph. * * <P>As an implementation note, this version traverses the CalValue graph by using the * Java call stack, and hence will throw a java.lang.StackOverflowError even for some moderately sized * structures, such as displaying an evaluated CAL list of 4000 elements. * * @param calValue cannot be null * @return a textual representation of calValue for debugging purposes. */ @SuppressWarnings(value={"unused"}) private static final String javaStackShowInternal(CalValue calValue) { if (calValue == null) { throw new NullPointerException("calValue cannot be null."); } calValue = unwrapOpaque(calValue); //javaStackShowInternal uses the java stack and will blow stack for CAL lists of size a few thousand. StringBuilder sb = new StringBuilder(); javaStackShowInternalHelper(calValue, sb); return sb.toString(); } private static final void javaStackShowInternalHelper(CalValue node, StringBuilder sb) { sb.append(node.debug_getNodeStartText()); for (int i = 0, nChildren = node.debug_getNChildren(); i < nChildren; ++i) { sb.append(node.debug_getChildPrefixText(i)); CalValue childNode = node.debug_getChild(i); if (childNode != null) { javaStackShowInternalHelper(childNode, sb); } else { sb.append("null"); } } sb.append(node.debug_getNodeEndText()); } /** * Returns a representation of the CalValue argument useful for debugging purposes. This representation * is subject to change and should not be relied upon in production code. Does not do any * evaluation or modify the CalValue in any way. * * <P>showInternalGraph does not traverse the CalValue graph by using the * Java call stack, and hence will not (in general) throw a java.lang.StackOverflowError. For example, * it can handle displaying evaluated CAL lists that are thousands of elements long. showInternalGraph * can also handle cyclic value graphs without failure. * * <P>Any exceptions (subclasses of Throwable) are caught and rethrown as a java.lang.RuntimeException with * cause set to the caught exception, and a message displaying a partial showInternalGraph result string * if one is available. The main cause of exceptions will be an exception in Object.toString() on one of the * foreign objects held onto by the CalValue graph. * * <P>showInternalGraph attempts to show more of the graph structure of the CalValue. If a node is shared, * its first appearance will be marked e.g. <@nodeNumber = nodeText>, and subsequent appearances will just * display as <@nodeNumber>. Indirections are shown using an asterisk *. * The extent to which this extra information is displayed is controllable by the showIndirectionNodes and * showAllSharedNodes parameters. * * @param calValues * @param showIndirectionNodes * if true, then indirection nodes will be displayed with an asterisk (*). If false, indirection nodes * will not display any text. * @param showAllSharedNodes * if true, then all nodes, including nodes with zero children such as Int value nodes and function nodes, * that are shared are displayed. Otherwise, only nodes having more than one child that are shared are shown. * Not showing all sharing hides certain trivial sharings and may make the output a bit easier to read in general. * @return a textual representation of the CalValue argument for debugging purposes. */ private static final String[] showInternalGraph(final CalValue[] calValues, final boolean showIndirectionNodes, final boolean showAllSharedNodes) { if (calValues == null) { throw new NullPointerException("calValues cannot be null."); } //showInternalGraph does not use the Java program stack in order to avoid running out of stack space. //for example, calling showInternal on a CAL list of a few thousand elements will run out of stack space. final String displayValues[] = new String [calValues.length]; StringBuilder sb = null; try { // We want to find out all the shared nodes across the array of CalValue. // If there is only one element in the array we can simply find the set for it. // Otherwise we need to bundle the array elements into a single graph. final CalValue calValue; if (calValues.length == 1) { calValue = unwrapOpaque(calValues[0]); calValues[0] = calValue; } else { //a helper class used to consider the argumentValues array as a single internal value, so that references //shared between elements of the array will be properly identified as being shared in the showInternal call. calValue = new CalValue () { @Override public int debug_getNChildren() { return calValues.length; } @Override public CalValue debug_getChild(int childN) { return calValues[childN]; } @Override public String debug_getNodeStartText() { return ""; } @Override public String debug_getNodeEndText() { return ""; } @Override public String debug_getChildPrefixText(int childN) { return " "; } @Override public boolean debug_isIndirectionNode() { return false; } /** * {@inheritDoc} */ @Override public final DataType getDataType() { return DataType.OTHER; } @Override public final CalValue internalUnwrapOpaque() { return this; } @Override public MachineType debug_getMachineType() { throw new UnsupportedOperationException("debug_getMachineType should never be called on this virtual CalValue"); } }; } //(CalValue -> Integer) nodes that occur more than once in the graph of the internal value mapped to //the node number to use when displaying them textually. final Map<CalValue, Integer> sharedNodeMap = calculateSharedNodeMap(calValue, showAllSharedNodes); //subset of the keys of sharedNodeMap that have already been visited so we don't need to display the value text //<@nodeNumber = nodeText> but only <@nodeNumber> final Set<CalValue> alreadyVisitedSharedNodeSet = new HashSet<CalValue>(); for (int z = 0; z < calValues.length; ++z) { sb = new StringBuilder(); //the stack will hold elements of type CalValue, as well as elements of type String. final ArrayStack<Object> stack = ArrayStack.make(); stack.push(calValues[z]); while (!stack.isEmpty()) { final Object value = stack.pop(); if (value == null) { //this can happen, since CalValue.debug_getChild can return null. //The reason for this is that RTValue.clearMembers() method may be called before setResult() is called so //the node is no longer an unmodified application node, but not yet an indirection. //For example, in a tail recursive function, the f method calls clearMembers right away, but setResult is //only called at the end. If some of the arguments to f refer to the application node themselves, then when //those are traced, they will encounter null children. sb.append("null"); } else if (value instanceof String) { sb.append(value); } else if (value instanceof CalValue) { final CalValue node = (CalValue)value; Object sharedNodeMapValue = sharedNodeMap.get(node); boolean isShared = sharedNodeMapValue != null; boolean secondOrMoreOccurrence = isShared && alreadyVisitedSharedNodeSet.contains(node); int sharedNodeNumber = isShared ? ((Integer)sharedNodeMapValue).intValue() : 0; if (isShared) { if (secondOrMoreOccurrence) { sb.append("<@").append(sharedNodeNumber).append('>'); //do not push children of shared nodes that have already been visited continue; } else { alreadyVisitedSharedNodeSet.add(node); } } final int nChildren = node.debug_getNChildren(); boolean isIndirectionNode = node.debug_isIndirectionNode(); if (isIndirectionNode && nChildren != 1) { throw new IllegalStateException("indirection nodes must have exactly 1 child"); } if (nChildren == 0) { //optimize the case of nChildren == 0 to avoid pushing getEndNodeText as a String, only to be immediately popped. if (isShared) { sb.append("<@").append(sharedNodeNumber).append(" = "); } sb.append(node.debug_getNodeStartText()); sb.append(node.debug_getNodeEndText()); if (isShared) { sb.append('>'); } } else { if (isShared) { sb.append("<@").append(sharedNodeNumber).append(" = "); } if (showIndirectionNodes && isIndirectionNode) { sb.append('*'); } sb.append(node.debug_getNodeStartText()); String nodeEndText; if (isShared) { nodeEndText = node.debug_getNodeEndText() + ">"; } else { nodeEndText = node.debug_getNodeEndText(); } if (nodeEndText.length() > 0) { stack.push(nodeEndText); } for (int i = nChildren - 1; i >= 0; --i) { CalValue childNode = node.debug_getChild(i); //note that childNode may be null. This is OK- we still push onto the stack. stack.push(childNode); String childPrefixText = node.debug_getChildPrefixText(i); if (childPrefixText.length() > 0) { stack.push(childPrefixText); } } } } } displayValues[z] = sb.toString(); } } catch (Throwable exception) { //catch any exception generated in calling showInternalGraph, in order to create a new exception that captures //any partially generated error text. StringBuilder message = new StringBuilder("(showInternalGraph failed with a ").append(exception.getClass().getName()); boolean partialResultFound = false; for (int i = 0; i < displayValues.length; ++i) { String s = displayValues[i]; if (s != null) { if (!partialResultFound) { message.append(". Partial result: "); partialResultFound = true; } message.append(s); } } if (sb != null && sb.length() > 0) { if (!partialResultFound) { message.append(". Partial result: "); partialResultFound = true; } message.append(sb.toString()); } if (partialResultFound) { message.append(")"); } else { message.append(".)"); } } return displayValues; } /** * @param calValue * @param showAllSharedNodes if true, all shared nodes are included in the returned map. Otherwise, only those * nodes with 1 or more children and included. * @return (CalValue -> Int). Map from nodes occurring multiple times in the calValue graph to an ordinal. * 1 will be for the first node in a pre-order traversal of calValue occurring more than once. * 2 will be for the second node in a pre-order traversal occurring twice etc. */ private static final Map<CalValue, Integer> calculateSharedNodeMap(CalValue calValue, boolean showAllSharedNodes) { // (CalValue -> Boolean) map from referentially distinct CalValue objects to a Boolean indicating // whether the CalValue occurs more than once in the graph. The order on the keys is according to a // pre-order traversal of the first occurrence of distinct CalValue objects in the graph. LinkedHashMap<CalValue, Boolean> distinctNodeMap = new LinkedHashMap<CalValue, Boolean>(); //the stack will hold elements of type CalValue ArrayStack<CalValue> stack = ArrayStack.make(); stack.push(calValue); while (!stack.isEmpty()) { CalValue node = stack.pop(); Object previousOccursMoreThanOnce = distinctNodeMap.get(node); boolean occursMoreThanOnce; if (previousOccursMoreThanOnce == null) { occursMoreThanOnce = false; distinctNodeMap.put(node, Boolean.FALSE); } else { occursMoreThanOnce = true; if (!((Boolean)previousOccursMoreThanOnce).booleanValue()) { distinctNodeMap.put(node, Boolean.TRUE); } } if (!occursMoreThanOnce) { final int nChildren = node.debug_getNChildren(); for (int i = nChildren - 1; i >= 0; --i) { CalValue childNode = node.debug_getChild(i); if (childNode != null) { stack.push(childNode); } } } } //(CalValue -> Int). Map from nodes occurring multiple times to an ordinal. 1 will be for the first //node in a pre-order traversal occurring more than once. 2 will be for the second node in a pre-order traversal //occurring more than once etc. //For example, if distinctNodeMap is [(n1, False), (n2, False), (n3, True), (n4, False), (n5, True)] //then multipleNodeMap is [(n3, 1), (n5, 2)] Map<CalValue, Integer> multipleNodeMap = new HashMap<CalValue, Integer>(); int multipleNodeNumber = 1; for (final Map.Entry<CalValue, Boolean> entry : distinctNodeMap.entrySet()) { CalValue node = entry.getKey(); if ((showAllSharedNodes || node.debug_getNChildren() > 0) && entry.getValue().booleanValue()) { multipleNodeMap.put(node, Integer.valueOf(multipleNodeNumber)); ++multipleNodeNumber; } } return multipleNodeMap; } /** * A helper function for tracing support. * * This is equivalent to displaying the individual space-separated arguments by just * calling showInternal, except that reference shared between separate arguments are displayed. * For example, in the trace for: * let x = [20, 30, 40 :: Int]; in deepSeq x x * The 2 arguments of deepSeq will show sharing in the trace such as: * Prelude.deepSeq <@1 = (Prelude.Cons 20 (Prelude.Cons 30 (Prelude.Cons 40 Prelude.Nil)))> <@1> * * @param argumentValues * @return String */ public static String showInternalForArgumentValues(final CalValue[] argumentValues) { if (argumentValues == null) { throw new NullPointerException(); } String[] results = showInternal(argumentValues); StringBuilder sb = new StringBuilder(); for (int i = 0; i < results.length; ++i) { sb.append(" "); sb.append(results[i]); } return sb.toString(); } }