/** * Return whether autoburning will result in a unification. * * @param destType The destination type to unify with * @param burnEntity on which to attempt "autoburning" * @param info the info to use * @return AutoburnLogic.AutoburnInfo autoburnable result */ public static AutoburnInfo getAutoburnInfo( TypeExpr destType, GemEntity burnEntity, TypeCheckInfo info) { // see if we have to do anything TypeExpr entityType; if (burnEntity == null || (entityType = burnEntity.getTypeExpr()) == null) { return AutoburnInfo.makeNoUnificationPossibleAutoburnInfo(); } return getAutoburnInfoWorker(destType, entityType.getTypePieces(), null, info, null); }
@Override public void commitValue() { IntellicutListEntry listEntry = (IntellicutListEntry) intellicutPanel.getIntellicutList().getSelectedValue(); if (listEntry != null) { GemEntity gemEntity = (GemEntity) listEntry.getData(); ModuleTypeInfo currentModuleInfo = valueEditorManager.getPerspective().getWorkingModuleTypeInfo(); TypeExpr valueNodeType = getValueNode().getTypeExpr(); TypeExpr functionType = gemEntity.getTypeExpr(); TypeExpr unifiedType; try { unifiedType = TypeExpr.unify(valueNodeType, functionType, currentModuleInfo); } catch (TypeException e) { throw new IllegalStateException(e.getMessage()); } GemEntityValueNode newValueNode = new GemEntityValueNode(gemEntity, unifiedType); replaceValueNode(newValueNode, true); } super.commitValue(); }
/** * See if we can reasonably use the values cached in the target's argument parts. For now this * just means if we can use the cached panels without performing any type conversions of any * sort. Precondition: target and inputToTypeExprMap are set Postcondition: none. * * @return boolean true if it's ok to use the values cached in the target's arguments. */ private boolean canUseCachedArguments() { List<Gem.PartInput> argParts = targetDisplayedGem.getTargetArguments(); // Get the input types. TypeExpr[] targetPieces = new TypeExpr[argParts.size()]; for (int i = 0; i < targetPieces.length; i++) { targetPieces[i] = argParts.get(i).getType(); } TypeExpr[] typesFromCachedArgs = new TypeExpr[targetPieces.length]; // Step through args. i'th arg corresponds to the i'th piece. // Note that there may be more pieces than args if the output type is a function for (int i = 0; i < targetPieces.length; i++) { // Get this input, and any cached value Gem.PartInput sinkPart = argParts.get(i); ValueNode cachedVN = gemCutter.getTableTop().getCachedValue(sinkPart); // there may be no cached value for this argument if (cachedVN != null) { typesFromCachedArgs[i] = cachedVN.getTypeExpr(); } } ModuleTypeInfo contextModuleTypeInfo = gemCutter.getPerspective().getWorkingModuleTypeInfo(); return TypeExpr.canPatternMatchPieces( typesFromCachedArgs, targetPieces, contextModuleTypeInfo); }
/** * Updates the text and tooltip of the placeholder label. * * @param polymorphicVarContext used for toString'ing the argument TypeExpr */ public void refreshDisplay(PolymorphicVarContext polymorphicVarContext) { TypeExpr leastConstrainedType = valueEntryPanel.getContext().getLeastConstrainedTypeExpr(); TypeExpr actualType = valueEntryPanel.getValueNode().getTypeExpr(); // Show the more specialized of the two types.. if (actualType instanceof TypeVar) { placeHolderLabel.setText( leastConstrainedType.toString(polymorphicVarContext, namingPolicy)); } else { placeHolderLabel.setText(actualType.toString(polymorphicVarContext, namingPolicy)); } if (getBorder() instanceof LineBorder) { setBorder(BorderFactory.createLineBorder(valueEntryPanel.getBackground(), 1)); } }
/** * Get a map from owner value node to type for the child editors used in this editor. * * @return Map For the owner value node of each child editor used by this editor. */ private Map<ValueNode, TypeExpr> getValueNodeToUnconstrainedTypeMap() { Map<ValueNode, TypeExpr> returnMap = new HashMap<ValueNode, TypeExpr>(); TypeExpr leastConstrainedType = getContext().getLeastConstrainedTypeExpr(); if (leastConstrainedType.rootTypeVar() != null) { leastConstrainedType = getDataConstructor().getTypeExpr().getResultType(); } returnMap.put(getValueNode(), leastConstrainedType); for (final DataConstructorEditorPanel childEditor : editorPanelList) { returnMap.putAll(childEditor.getValueNodeToUnconstrainedTypeMap(leastConstrainedType)); } return returnMap; }
/** * Builds a map from value node to type expression for the value node this panel is editing and * the owner value nodes of each value entry panel. * * @param leastConstrainedType the least constrained type that is allowed * @return the resulting map */ Map<ValueNode, TypeExpr> getValueNodeToUnconstrainedTypeMap(TypeExpr leastConstrainedType) { Map<ValueNode, TypeExpr> returnMap = new HashMap<ValueNode, TypeExpr>(); DataConstructor dataCons = getDataConstructor(); returnMap.put(valueNode, leastConstrainedType); for (int i = 0; i < editorPanels.length; i++) { ValueNode argValueNode = editorPanels[i].getValueEntryPanel().getOwnerValueNode(); TypeExpr argType = TypeExpr.getComponentTypeExpr(leastConstrainedType, i, dataCons); returnMap.put(argValueNode, argType); } return returnMap; }
/** * Filters the list returned by getAvailableInputTypes to only return the types that can actually * pattern match with the value node type expression. * * @return list of input types to be displayed in the switch type list */ protected Set<TypeExpr> getMatchingInputTypes() { Set<TypeExpr> dataTypes = getAvailableInputTypes(); Set<TypeExpr> matchingTypes = new HashSet<TypeExpr>(); ModuleTypeInfo currentModuleTypeInfo = valueEditorManager.getPerspective().getWorkingModuleTypeInfo(); if (currentModuleTypeInfo != null) { TypeExpr valueNodeTypeExpr = getValueNode().getTypeExpr(); for (final TypeExpr typeExpr : dataTypes) { if (TypeExpr.canPatternMatch(typeExpr, valueNodeTypeExpr, currentModuleTypeInfo)) { matchingTypes.add(typeExpr); } } } return matchingTypes; }
/** * Display value entry controls for all unbound arguments. Precondition: the target must be set. */ private void displayArgumentControls() { TableTopPanel tableTopPanel = getTableTopPanel(); // Build the list of value entry panels and place them onto the TableTop // Get the list of unbound arguments List<Gem.PartInput> argParts = targetDisplayedGem.getTargetArguments(); // see if it's ok to use the cached argument values boolean canUseCachedArgs = canUseCachedArguments(); // Figure out the types of the argument panels. Note that we must account for cached values int numArgs = argParts.size(); // make copies of the input types. TypeExpr[] inputTypes = new TypeExpr[numArgs]; for (int i = 0; i < numArgs; i++) { // Get this input, and its type Gem.PartInput inputPart = argParts.get(i); inputTypes[i] = inputPart.getType(); } TypeExpr[] specializedInputTypes; if (canUseCachedArgs) { // this is the array of types cached for each argument. If there is no cached type, we just // use the argument type. TypeExpr[] cachedTypes = new TypeExpr[numArgs]; for (int i = 0; i < numArgs; ++i) { // What we do next depends on whether we use cached sink values Gem.PartInput inputPart = argParts.get(i); ValueNode cachedVN = gemCutter.getTableTop().getCachedValue(inputPart); if (cachedVN != null) { cachedTypes[i] = cachedVN.getTypeExpr(); } } try { ModuleTypeInfo contextModuleTypeInfo = gemCutter.getPerspective().getWorkingModuleTypeInfo(); specializedInputTypes = TypeExpr.patternMatchPieces(cachedTypes, inputTypes, contextModuleTypeInfo); } catch (TypeException te) { throw new IllegalStateException("Error reusing cached args."); } } else { specializedInputTypes = TypeExpr.copyTypeExprs(inputTypes); } ValueEditorManager valueEditorManager = gemCutter.getValueEditorManager(); ValueEditorDirector valueEditorDirector = valueEditorManager.getValueEditorDirector(); Map<PartInput, ValueEditor> inputToEditorMap = new LinkedHashMap<PartInput, ValueEditor>(); // Step through args, create a new value input panel for each, with the correct value type for (int i = 0; i < numArgs; i++) { // Get the gem and input final Gem.PartInput inputPart = argParts.get(i); final Gem inputGem = inputPart.getGem(); int argumentNumber = inputPart.getInputNum(); QualifiedName scName = null; if (inputGem instanceof FunctionalAgentGem) { scName = ((FunctionalAgentGem) inputGem).getName(); } // What we do next depends on whether we use cached sink values ValueNode cachedVN; final ValueEditor editor; if (canUseCachedArgs && (cachedVN = gemCutter.getTableTop().getCachedValue(inputPart)) != null) { // use cached sink value to generate the VEP // get a copy of the cached VN but with the new type expr ValueNode newVN = cachedVN.copyValueNode(); // instantiate the VEP with the new VN (which has the old cached value) editor = valueEditorDirector.getRootValueEditor( valueEditorHierarchyManager, newVN, scName, argumentNumber, new GemCutterMetadataRunner(gemCutter, inputGem)); } else { // don't use cached sink value. Generate the default VEP for this sink. TypeExpr inputType = specializedInputTypes[i]; editor = valueEditorDirector.getRootValueEditor( valueEditorHierarchyManager, inputType, scName, argumentNumber, new GemCutterMetadataRunner(gemCutter, inputGem)); } // If this is a value entry panel then set the name of the input for use in tooltips if (editor instanceof ValueEntryPanel) { ((ValueEntryPanel) editor) .setArgumentName(inputPart.getArgumentName().getCompositeName()); } // Position the editor next to the connection point. positionEditor(editor, inputPart); // Now add the editor to the table top. tableTopPanel.add(editor, 0); // Add to the list of active entry panels valueEditorHierarchyManager.addTopValueEditor(editor); // Update the context and editor map editor.setContext( new ValueEditorContext() { public TypeExpr getLeastConstrainedTypeExpr() { return inputPart.getType(); } }); inputToEditorMap.put(inputPart, editor); // If the editor resizes because it's value changes, make sure it stays aligned to the // connection editor.addComponentListener( new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { positionEditor((ValueEditor) e.getComponent(), inputPart); } }); } // Must prevent ValueGems from being editable. tableTopPanel.setValueGemsEnabled(false); // Need to activate the value editor hierarchy to ensure correct highlighting. valueEditorHierarchyManager.activateCurrentEditor(); // Set the context and type switch listener for each new editor. ArgumentValueCommitter argumentValueCommitter = new ArgumentValueCommitter(valueEditorManager, inputToEditorMap); for (final ValueEditor argumentEditor : getArgumentPanels()) { argumentEditor.addValueEditorListener(argumentValueCommitter); } // May need to show argument controls. if (numArgs > 0) { gemCutter.showArgumentControls(inputToEditorMap); gemCutter.getResetAction().setEnabled(true); } else { gemCutter.getResetAction().setEnabled(false); } }
/** * Note that the types of combinations considered is subject to MAX_BURN_DEPTH. * * <p>TODOEL: it seems like it would be better if all of the input types in source type pieces * were burnable, and it were up to the caller to figure out how the resulting indices mapped onto * its arguments. * * @param destType for example, if connecting to the first argument of the map gem, this would be * a -> b * @param sourceTypePieces a list of the relevant source type pieces (ie inputs and output). This * includes the burnable and already burnt inputs in proper order, as well as the output piece * (which should appear at the end). It does NOT include any inputs that are already bound. * For example, in the following case: * <p>Int --\ | \ bound ---------- \ | |- a Boolean (burned) >- / | / Double --/ * <p>This would be [Int, Boolean, Double, a] * @param unburnedArguments This is an array of indices into sourceTypePieces that should be * considered for burning sites. This can be null, in which case *every* argument is * interpreted as a possible burn site i.e. equivalent to a value of [0, 1, 2, ..., * sourceTypePieces.length - 1]. For example, if the take gem were the source, and it had no * arguments burnt and none bound, this would be [0, 1]. If its 0th argument were burnt this * would be [1]. If both arguments were burnt, this would be []. Note that since these are * indices into sourceTypePieces, if one or more of the arguments are bound, they are not * counted. So, for example, if the first argument of take was bound to another gem and the * second one was unburned, this would be [0]. In the situation illustrated above, this would * be [0, 2]. * @param info the typeCheck info to use * @param sourceGem the source gem, if one exists. Should be null if otherwise. */ private static AutoburnInfo getAutoburnInfoWorker( TypeExpr destType, TypeExpr[] sourceTypePieces, int[] unburnedArguments, TypeCheckInfo info, Gem sourceGem) { // Possible improvements: // Check matchability of the nth argument burn to the nth dest type piece. // Use the fact that some clients do not want to know all burn combos in the ambiguous case. // Use type piece identity (eg. source type pieces which are the same type var..). Hash for // gem types already tried. // Group types when calculating (eg. if try burning one Double, trying to burn the other // double will give the same result). // Specialize as burns are applied (eg. makeTransform is Typeable a => a -> a. If one a is // specialized, so is the other). // initialize our return type AutoburnUnifyStatus unificationStatus = AutoburnUnifyStatus.NOT_POSSIBLE; int numUnburned; if (unburnedArguments == null) { // all positions are burnable numUnburned = sourceTypePieces.length - 1; unburnedArguments = new int[numUnburned]; for (int i = 0; i < numUnburned; i++) { unburnedArguments[i] = i; } } else { // only some positions are burnable. This corresponds to a gem graph where some arguments have // been already burnt // by the user, or where some of the arguments do not correspond to arguments in the root gem. numUnburned = unburnedArguments.length; } TypeExpr outputType = sourceTypePieces[sourceTypePieces.length - 1]; boolean doNotTryBurning = false; if (!destType.isFunctionType()) { // Don't try burning if the destination type isn't a function. // This is a not a logically required restriction. For example, any burnt gem can be connected // to the Prelude.id gem. // A less trivial example is that Prelude.sin with argument burnt can be connected to the // Prelude.typeOf gem. This is because // Double -> Double can unify with Typeable a => a. // However, it is not a situation that the end user would "likely" want to see in the list and // we found that it results in // many rare situations. doNotTryBurning = true; } else { // If the destination result type is a type constructor, perform a basic check against the // source result type. // The fact used here: // a. for 2 types to unify, their result types must unify. // b. burning doesn't change the result type (in the technical sense of CAL, and not in the // sense of the output type displayed // as the result of a gem in the GemCutter. For example, burning the first argument of take // gem changes the gem to // in the GemCutter to display as a 1 argument gem with output type Int -> [a], but the // overall type of the burnt take gem is // is [a] -> (Int -> [a]) which is just [a] -> Int -> [a] (-> is right associative) and has // result type [a], as with the unburnt // take gem. TypeExpr destResultTypeExpr = destType.getResultType(); if (destResultTypeExpr.rootTypeVar() == null && !TypeExpr.canUnifyType( destResultTypeExpr, outputType.getResultType(), info.getModuleTypeInfo())) { // only do the check if the destination result type is a type constructor or record type. // the reason for this is that if it is a parameteric type, we are likely to succeed here... doNotTryBurning = true; } } int maxTypeCloseness = -1; // the type closeness with no burning int noBurnTypeCloseness = Integer.MIN_VALUE; // The lower bound on the number of burns in the upper range. int upperRangeMinBurns = Math.max(numUnburned - MAX_BURN_DEPTH, MAX_BURN_DEPTH + 1); // List of all burn combinations for which unification can take place. List<BurnCombination> burnCombos = new ArrayList<BurnCombination>(); // Our combination generator will create all combinations of a fixed size. // So, we iterate over all possible sizes. // However, we skip over sizes between MAX_BURN_DEPTH and upperRangeMinBurns // so that our algorithm runs in polynomial ( specifically O(n^MAX_BURN_DEPTH) ) time // rather than exponential ( O(2^n) ). for (int numBurns = 0; numBurns <= numUnburned; numBurns++) { if (doNotTryBurning && numBurns > 0) { break; } // If necessary, skip from MAX_BURN_DEPTH to upperRangeMinBurns. if (numBurns == MAX_BURN_DEPTH + 1) { numBurns = upperRangeMinBurns; } // Iterate through all the combinations of burning inputs of size numBurns. boolean isValidCombo = true; for (int[] burnComboArray = getFirstCombination(numBurns); isValidCombo; isValidCombo = getNextCombination(burnComboArray, numUnburned)) { // calculate what the corresponding output type would be. // Note that we are assuming that the elements of burnComboArray are in // ascending order. int currentSourceTypePiece = sourceTypePieces.length - 2; int nextUnburnedArg = numUnburned - 1; int nextArgToBurn = numBurns - 1; // Loop over currentSourceTypePieces. // These represent all the burned and unburned arguments. TypeExpr newOutputType = outputType; while (currentSourceTypePiece >= 0) { boolean shouldBurn = false; if (nextUnburnedArg >= 0 && currentSourceTypePiece == unburnedArguments[nextUnburnedArg]) { // This is an unburned argument. if (nextArgToBurn >= 0 && nextUnburnedArg == burnComboArray[nextArgToBurn]) { // This is a burnable argument that we want to try burning shouldBurn = true; nextArgToBurn--; } nextUnburnedArg--; } else { // This is a burned argument. shouldBurn = true; } if (shouldBurn) { // We need to add the corresponding type to the output type TypeExpr argType = sourceTypePieces[currentSourceTypePiece]; newOutputType = TypeExpr.makeFunType(argType, newOutputType); } currentSourceTypePiece--; } // Would the resulting output type unify with the type we want? int typeCloseness = TypeExpr.getTypeCloseness(destType, newOutputType, info.getModuleTypeInfo()); if (numBurns == 0) { assert (noBurnTypeCloseness == Integer.MIN_VALUE); noBurnTypeCloseness = typeCloseness; } if (typeCloseness >= 0) { if (typeCloseness > maxTypeCloseness) { maxTypeCloseness = typeCloseness; } boolean autoburnable = false; if (numBurns == 0) { // We didn't have to autoburn. unificationStatus = AutoburnUnifyStatus.NOT_NECESSARY; } else if (unificationStatus == AutoburnUnifyStatus.NOT_POSSIBLE) { // We have our first valid combo and it is not possible to unify without burning. autoburnable = true; unificationStatus = AutoburnUnifyStatus.UNAMBIGUOUS; } else if (unificationStatus == AutoburnUnifyStatus.NOT_NECESSARY) { // We have our first valid combo and it is possible to unify without burning. autoburnable = true; unificationStatus = AutoburnUnifyStatus.UNAMBIGUOUS_NOT_NECESSARY; } else { // We got > 1 valid combos. autoburnable = true; if (unificationStatus == AutoburnUnifyStatus.UNAMBIGUOUS) { unificationStatus = AutoburnUnifyStatus.AMBIGUOUS; } else if (unificationStatus == AutoburnUnifyStatus.UNAMBIGUOUS_NOT_NECESSARY) { unificationStatus = AutoburnUnifyStatus.AMBIGUOUS_NOT_NECESSARY; } } // If unify can take place, copy the array and store it in burn combos. if (autoburnable) { int[] autoburnableCombo; if (sourceGem != null) { autoburnableCombo = translateBurnCombo(burnComboArray, sourceGem); } else { autoburnableCombo = new int[numBurns]; System.arraycopy(burnComboArray, 0, autoburnableCombo, 0, numBurns); } burnCombos.add(new BurnCombination(autoburnableCombo, typeCloseness)); } } } } if (unificationStatus == AutoburnUnifyStatus.NOT_POSSIBLE) { burnCombos = null; } return new AutoburnInfo(unificationStatus, burnCombos, maxTypeCloseness, noBurnTypeCloseness); }
/** * Return whether autoburning will result in a unification. Only inputs on the given gem will be * considered for burning. * * @param destType The destination type to unify with. * @param gem The gem on which to attempt "autoburning". * @param info the typeCheck info to use. * @return AutoburnLogic.AutoburnInfo autoburnable result. */ public static AutoburnInfo getAutoburnInfo(TypeExpr destType, Gem gem, TypeCheckInfo info) { // see if we got a real type if (destType == null) { return AutoburnInfo.makeNoUnificationPossibleAutoburnInfo(); } int[] burnableArgIndices; List<TypeExpr> sourceTypeList = new ArrayList<TypeExpr>(); // input types, then output type. TypeExpr outType = (gem instanceof CollectorGem) ? ((CollectorGem) gem).getCollectingPart().getType() : gem.getOutputPart().getType(); TypeExpr[] outTypePieces = outType.getTypePieces(); // A collector gem's collecting part is not burnable. if (gem instanceof CollectorGem && !((CollectorGem) gem).isConnected()) { burnableArgIndices = new int[0]; } else { // declare the burnt arg indices array burnableArgIndices = new int[gem.getNInputs()]; if (burnableArgIndices.length != 0) { // get the unbound input parts of the gem and keep track of which ones are burnable int unburnedCount = 0; int burnedCount = 0; for (int i = 0, n = gem.getNInputs(); i < n; i++) { Gem.PartInput input = gem.getInputPart(i); if (!input.isConnected()) { if (input.isBurnt()) { sourceTypeList.add(outTypePieces[burnedCount]); burnedCount++; } else { sourceTypeList.add(input.getType()); burnableArgIndices[unburnedCount] = sourceTypeList.size() - 1; unburnedCount++; } } } // Calculate what the output type would be if none of the arguments were burned. TypeExpr newOutType = outTypePieces[outTypePieces.length - 1]; for (int i = outTypePieces.length - 2; i >= burnedCount; i--) { newOutType = TypeExpr.makeFunType(outTypePieces[i], newOutType); } outType = newOutType; // Assign to a new trimmed down array int[] newIndices = new int[unburnedCount]; System.arraycopy(burnableArgIndices, 0, newIndices, 0, unburnedCount); burnableArgIndices = newIndices; } } // Add the output type to the source types. sourceTypeList.add(outType); return getAutoburnInfoWorker( destType, sourceTypeList.toArray(new TypeExpr[0]), burnableArgIndices, info, gem); }