public interface PipeMaker {
	/** Returns the signature as relationName + "/" + arity */
	public String getSignature();

	/** Given the bindings, this method returns a PixyPipe and adds any new replacements typically of the form x -> $x */
	public PixyPipe makePipe(List<PixyDatum> bindings, Map<String, PixyDatum> replacements, VariableGenerator varGen);
} public class PixyDatum {
	public static final int BIGDECIMAL_SCALE = 10;

	public static final String FAIL = "fail";
	public static final String TRUE = "true";
	public static final String FALSE = "false";
	public static final String CUT = "!";
	
	private PixyDatumType type; 
    private BigDecimal num;
    private String str;
    private String var;
    private String atom;
    private List<PixyDatum> list;
    private String relName;
    private List<PixyDatum> relTuple;
    
    public PixyDatum(PixyDatumType type, BigDecimal num) {
    	assert type == PixyDatumType.NUMBER;
    	this.type = type;
    	this.num = num;
    }

    public PixyDatum(PixyDatumType type, String s) {
    	this.type = type;
    	if (type == PixyDatumType.VARIABLE) {
			this.var = s;
    	} else if (type == PixyDatumType.SPECIAL_ATOM) {
    		this.atom = s;
    	} else if (type == PixyDatumType.STRING) {
    		this.str = s;
    	} else {
    		assert true;
    	}
    }

    public PixyDatum(PixyDatumType type, List<PixyDatum> list) {
    	assert type == PixyDatumType.LIST;
    	this.type = type;
    	this.list = list;
    }

    public PixyDatum(PixyDatumType type, String relName, List<PixyDatum> relTuple) {
    	assert type == PixyDatumType.RELATION;
    	
    	this.type = type;
    	this.relName = relName;
    	this.relTuple = relTuple;
    }
    
    public PixyDatum(AbstractPrologTerm term) {
		if ((term instanceof PrologIntegerNumber) || (term instanceof PrologFloatNumber)) {
			this.type = PixyDatumType.NUMBER;
			this.num = new BigDecimal(term.getText());
		} else if (term instanceof PrologAtom) {
			// Special atom handling
			if (term.getText().equals(FAIL)) {
				this.type = PixyDatumType.SPECIAL_ATOM;
				this.atom = FAIL;
			} else if (term.getText().equals(FALSE)) {
				this.type = PixyDatumType.SPECIAL_ATOM;
				this.atom = FALSE;
			} else if (term.getText().equals(TRUE)) {
				this.type = PixyDatumType.SPECIAL_ATOM;
				this.atom = TRUE;
			} else {
				this.type = PixyDatumType.STRING;
				this.str = term.getText();
			}
		} else if (term instanceof PrologVariable) {
			this.type = PixyDatumType.VARIABLE;
			this.var = term.getText();
		} else if (term instanceof PrologList) {
			PrologList pList = (PrologList)term;
			this.type = PixyDatumType.LIST;
			this.list = new ArrayList<PixyDatum>();
			
			while (!pList.isNullList()) {
				AbstractPrologTerm pHead = pList.getHead();
				
				if (pHead instanceof PrologVariable) {
					throw new PixyException(PixyErrorCodes.VARIABLES_NOT_SUPPORTED_INSIDE_LISTS, "Encountered variable " + pHead.getText() + " in " + term.toString());
				}
				
				list.add(new PixyDatum(pList.getHead()));
				
				pList = (PrologList)pList.getTail();
			}
		} else if ((term instanceof PrologStructure) && term.getText().equals("!")) {
			this.type = PixyDatumType.SPECIAL_ATOM;
			this.atom = CUT;
		} else if (term instanceof PrologStructure) {
			this.type = PixyDatumType.RELATION;
			this.relName = term.getText();
			this.relTuple = new ArrayList<PixyDatum>();
			
			PrologStructure structure = ((PrologStructure)term);

			// Check for not
			if (relName.equals("not")) {
				if (structure.getArity() != 1) {
					throw new PixyException(PixyErrorCodes.INVALID_NOT_PARAMETERS, "Encountered: " + structure.toString() + " with arity " + structure.getArity());
				}

				// not(something(with, params))
				AbstractPrologTerm notTerm = structure.getElement(0);
				if (notTerm instanceof PrologStructure) {
					// The relation names of the form not(something) will be handled internally
					relName = "not(" + notTerm.getText() + ")";
					structure = (PrologStructure)notTerm;
				} else {
					throw new PixyException(PixyErrorCodes.INVALID_NOT_PARAMETERS, "Encountered: " + structure.toString() + " which is not a relation");
				}
			}

			// Go over the terms
			for (int idx = 0; idx < structure.getArity(); idx++) {
				AbstractPrologTerm subStruct = structure.getElement(idx);
				PixyDatum subExpr = new PixyDatum(subStruct);
				PixyDatumType subExprType = subExpr.getType(); 
				if ((subExprType != PixyDatumType.SPECIAL_ATOM) 
						&& (subExprType != PixyDatumType.STRING) 
						&& (subExprType != PixyDatumType.NUMBER) 
						&& (subExprType != PixyDatumType.VARIABLE)
						&& (subExprType != PixyDatumType.LIST)) {
					throw new PixyException(PixyErrorCodes.ENCOUNTERED_COMPLEX_RELATION, "Encountered: " + subStruct.toString() + " inside " + structure.toString());					
				} else {
					relTuple.add(subExpr);
				}
			}
		}
	}
	
	public PixyDatumType getType() {
		return type;
	}
	
	public BigDecimal getNumber() {
		return num;
	}

	public String getRelationName() {
		return relName;
	}
	
	public int getArity() {
		return relTuple.size();
	}
	
	public List<PixyDatum> getRelationTuple() {
		return relTuple;
	}

	public boolean isFail() {
		return (getType() == PixyDatumType.SPECIAL_ATOM) && (atom.equals(FAIL) || atom.equals(FALSE)); 
	}

	public boolean isTrue() {
		return (getType() == PixyDatumType.SPECIAL_ATOM) && atom.equals(TRUE); 
	}

	public boolean isCut() {
		return (getType() == PixyDatumType.SPECIAL_ATOM) && atom.equals("!"); 
	}

	public boolean isGround() {
		return (type == PixyDatumType.NUMBER)
			|| (type == PixyDatumType.SPECIAL_ATOM)
			|| (type == PixyDatumType.STRING)
			|| (type == PixyDatumType.LIST);
	}


	public boolean isFixed() {
		return (type == PixyDatumType.NUMBER)
		|| ((type == PixyDatumType.SPECIAL_ATOM) && !isAtomAPipeVar())
		|| (type == PixyDatumType.STRING)
		|| (type == PixyDatumType.LIST);
	}

	public String getVarName() {
		return var;
	}

	public PixyDatum renameToUniqueVars(Map<String, String> varMap, VariableGenerator varGen) {
		switch (type) {
		case NUMBER:
		case SPECIAL_ATOM:
		case STRING:
			return this;
			
		case LIST:
			// Rename its items
			List<PixyDatum> newList = new ArrayList<PixyDatum>();
			for (PixyDatum listItem : list) {
				newList.add(listItem.renameToUniqueVars(varMap, varGen));
			}
			return new PixyDatum(PixyDatumType.LIST, newList);
			
		case VARIABLE:
			// Return the mapped variable, if defined, or a new one
			String mappedVar = varMap.get(var);
			if (mappedVar == null) {
				mappedVar = varGen.newVariable(var);
				
				// The variable _ should always be renamed to unique
				if (!var.equals("_")) {
					varMap.put(var, mappedVar);
				}
			}
			
			return new PixyDatum(PixyDatumType.VARIABLE, mappedVar);
			
		case RELATION:
			List<PixyDatum> tuple = new ArrayList<PixyDatum>(); 
			for (PixyDatum field : relTuple) {
				tuple.add(field.renameToUniqueVars(varMap, varGen));
			}

			return new PixyDatum(PixyDatumType.RELATION, relName, tuple);
			
		default:
			throw new PixyException(PixyErrorCodes.INTERNAL_ERROR, "Unhandled type " + type);
		}
	}

	public PixyDatum replaceVars(Map<String, PixyDatum> replacements) {
		switch (type) {
		case NUMBER:
		case SPECIAL_ATOM:
		case STRING:
			return this;
			
		case LIST: 
			// Replace its items
			List<PixyDatum> newList = new ArrayList<PixyDatum>();
			for (PixyDatum listItem : list) {
				newList.add(listItem.replaceVars(replacements));
			}
			return new PixyDatum(PixyDatumType.LIST, newList);
			
		case VARIABLE:
			// Return the mapped variable, if defined, or a new one
			PixyDatum replacement = replacements.get(var);
			if (replacement == null) {
				return this;
			} else {
				// Using recursion to replace more than once
				return replacement.replaceVars(replacements);
			}

		case RELATION:
			List<PixyDatum> tuple = new ArrayList<PixyDatum>(); 
			for (PixyDatum field : relTuple) {
				tuple.add(field.replaceVars(replacements));
			}

			return new PixyDatum(PixyDatumType.RELATION, relName, tuple);

		default:
			throw new PixyException(PixyErrorCodes.INTERNAL_ERROR, "Unhandled type " + type);
		}		
	}
	
	public String toString() {
		switch (type) {
		case NUMBER:
			return num.toString();

		case SPECIAL_ATOM:
			return atom;

		case STRING:
			return "'" + str + "'";
			
		case VARIABLE:
			return var;

		case RELATION:
			return relName + relTuple;

		case LIST:
			return list.toString();
			
		default:
			throw new PixyException(PixyErrorCodes.INTERNAL_ERROR, "Unhandled type " + type);
		}		
	}

	public boolean isAtomAPipeVar() {
		return (type == PixyDatumType.SPECIAL_ATOM) && atom.startsWith("$");
	}

	public String getAtom() {
		return atom;
	}

	public String getAtomVarName() {
		assert isAtomAPipeVar(); 

		return atom.substring(1);
	}

	public String getString() {
		return str;
	}

	public List<PixyDatum> getList() {
		return list;
	}

	public static boolean isNumber(Object x) {
		return (x instanceof Integer)
			|| (x instanceof Long)
			|| (x instanceof Short)
			|| (x instanceof Byte)
			|| (x instanceof BigInteger)
			|| (x instanceof Float)
			|| (x instanceof Double);
	}
	
	public static BigDecimal convertToBigDecimal(Object x) {
		if (x instanceof Integer) {
			return new BigDecimal((Integer)x);
		} else if (x instanceof Long) {
			return new BigDecimal((Long)x);
		} else if (x instanceof BigInteger) {
			return new BigDecimal((BigInteger)x);
		} else if (x instanceof Float) {
			return new BigDecimal((Float)x);
		} else if (x instanceof Double) {
			return new BigDecimal((Double)x);
		} else if (x instanceof Byte) {
			return new BigDecimal((Byte)x);
		} else if (x instanceof Short) {
			return new BigDecimal((Short)x);
		} else {
			try {
				return new BigDecimal(x.toString());
			} catch (NumberFormatException e) {
				throw new PixyException(PixyErrorCodes.INVALID_NUMBER, "Encountered: " + x);
			}
		}
	}
	
	@Override
	public boolean equals(Object o) {
		if (o instanceof PixyDatum) {
			PixyDatum pd = (PixyDatum) o;

			return type.equals(pd.getType()) 
			  && toString().equals(pd.toString());
		} else {
			return false;
		}
	}
} public enum PixyDatumType {
	SPECIAL_ATOM, // fail 
	NUMBER, 
	STRING, 
	VARIABLE, 
	RELATION,
	LIST
} public enum PixyErrorCodes {
    INTERNAL_ERROR {
        public String toString() {
            return "INTERNAL_ERROR: An internal error occurred";
        }
    },
    
    VARIABLES_NOT_SUPPORTED_INSIDE_LISTS {
        public String toString() {
            return "VARIABLES_NOT_SUPPORTED_INSIDE_LISTS: Pixy doesn't support variables inside lists";
        }
    },
    
    INVALID_NOT_PARAMETERS {
        public String toString() {
            return "NOT_ARITY_IS_NOT_ONE: Expecting exactly one parameter inside not(). The parameter must be a relation. ";
        }
    },
    
    ENCOUNTERED_COMPLEX_RELATION {
        public String toString() {
            return "ENCOUNTERED_COMPLEX_RELATION: Pixy does not support relations inside other relations, with the exception of not(relation()). All parameters to a relation must be simple strings, numbers, variables, lists or special atoms (fail, true, false). ";
        }
    },

    INVALID_NUMBER {
        public String toString() {
            return "INVALID_NUMBER: The given string could not be converted to a number";
        }
    },
    
    OPERATION_NOT_SUPPORTED {
        public String toString() {
            return "OPERATION_NOT_SUPPORTED: The given operation is not supported";
        }
    },
    
    OPERATION_NOT_SUPPORTED_IN_CONTEXT {
        public String toString() {
            return "OPERATION_NOT_SUPPORTED_IN_CONTEXT: The given operation is not supported in the current context";
        }
    },

    CAN_NOT_UNIFY_RELATIONS_OR_LISTS {
        public String toString() {
            return "CAN_NOT_UNIFY_RELATIONS_OR_LISTS: Pixy does not support unification of relations or lists. Please rewrite your '=' or 'is' rule";
        }
    },
    
    INVALID_TERM_IN_EXPRESSION {
        public String toString() {
            return "INVALID_TERM_IN_EXPRESSION: The given term is invalid inside an expression. Pixy only supports numbers, strings, variables and operations in its expressions";
        }
    },
    
    INVALID_TERM_IN_CLAUSE_BODY {
        public String toString() {
            return "INVALID_TERM_IN_CLAUSE_BODY: A clause body should only contain relations, boolean expressions or special atoms (like fail, true, false)";
        } 
    }, 
    
    INVALID_QUERY_FORM {
        public String toString() {
            return "INVALID_QUERY_FORM: A Pixy query should be of the form <predicate>(<param 1>, ..., <param N). ";
        }
    },
    
    INVALID_QUERY_PAREMETER {
        public String toString() {
            return "INVALID_QUERY_PAREMETER: The parameters to a Pixy query should be one of ?, _, &, $, &<named step>, $<named step>";
        }
    },
    
    QUERY_PLACEHOLDER_MISMATCH {
        public String toString() {
            return "QUERY_PLACEHOLDER_MISMATCH: A call to makePipe(<query>, values...) did not supply the same number of values as the number of place-holders (represented as ?) in the query";
        }
    },
    
    CAN_NOT_REMOVE_MISSING_RULE {
        public String toString() {
            return "CAN_NOT_REMOVE_MISSING_RULE: A given rule can not be removed because it is missing in the current list of available rules";
        }
    },
    
    PROLOG_PARSE_ERROR  {
        public String toString() {
            return "PROLOG_PARSE_ERROR: The Java PROLOG parser threw the given exception";
        }
    },
    
    RECURSION_NOT_SUPPORTED {
        public String toString() {
            return "RECURSION_NOT_SUPPORTED: Pixy does not support recursion (yet). Please remove the given cyclic dependency among rules";
        }
    },
    
    REFERENCE_TO_MISSING_RULE {
        public String toString() {
            return "REFERENCE_TO_MISSING_RULE: The given rule was referenced but not defined in the theory";
        }
    },
    
    SEQUENCING_ERROR {
        public String toString() {
            return "SEQUENCING_ERROR: Pixy tries to sequence the terms in the body of a clause, say Head :- rel1(Params1), rel2(Params2), ..., such that all the relations fully evaluate the variables passed to them. This sequencing process failed. Typically this error occurs because of typos in variable names or insufficient bound parameters passed to the rule";
        }
    },
    
    EXPECTING_A_PIPE {
        public String toString() {
            return "EXPECTING_A_PIPE: A pre-defined rule (such as in, out) is expecting a parameter to be a pipe";
        }
    },
    
    EXPECTING_A_STRING {
        public String toString() {
            return "EXPECTING_A_STRING: Expecting a parameter to be a string";
        }
    },
    
    EXPECTING_A_STRING_OR_LIST_OF_STRINGS {
        public String toString() {
            return "EXPECTING_A_STRING_OR_LIST_OF_STRINGS: Expecting a parameter (such as label) to be a string or a list-of-strings";
        }
    },
    
    EXPECTING_A_STRING_OR_NUMBER {
        public String toString() {
            return "EXPECTING_A_STRING_OR_NUMBER: Expecting a parameter to be a string or a number";
        }
    },
    
    EXPECTING_A_STRING_NUMBER_OR_BOOLEAN {
        public String toString() {
            return "EXPECTING_A_STRING_NUMBER_OR_BOOLEAN: Expecting a parameter to be a string, a number, or a boolean value";
        }
    },

    EXPECTING_A_NUMBER_OR_LIST_OF_TWO_NUMBERS {
    	public String toString() {
            return "EXPECTING_A_NUMBER_OR_LIST_OF_TWO_NUMBERS: Expecting a parameter to be a number, or a list of two numbers";
    	}
    }, 

    COALESCE_FAILURE {
        public String toString() {
            return "COALESCE_FAILURE: A coalesce or eval step failed because the referenced named step(s) don't have a value. Typically, this is caused by null values assigned to Pixy variables";
        }
    },

    MISSING_NAMED_STEP {
        public String toString() {
            return "MISSING_NAMED_STEP: The named step provided to a custom Gremlin pipe implemented in Pixy is not valid. This typically indicates a bug, or an invalid expression";
        }
    },
    
    TYPE_MISMATCH_IN_EVAL {
        public String toString() {
            return "TYPE_MISMATCH_IN_EVAL: An eval operation could not be performed because the types of the operands don't match the types supported by the operation. This is commonly an error in an expression";
        }
    },
} public class PixyException extends RuntimeException  {
    private static final long serialVersionUID = -5310572247323732287L;
    PixyErrorCodes code;

    public PixyException(PixyErrorCodes code) {
        super(code.toString());

        this.code = code;
    }
    
    public PixyException(PixyErrorCodes code, String s) {
        super(code.toString() + ". " + s);

        this.code = code;
    }
    
    public PixyException(PixyErrorCodes code, String s, Throwable t) {
        super(code.toString() + ". " + s, t);
        
        this.code = code;
    }
    
    public PixyErrorCodes getErrorCode() {
        return code;
    }
} public interface PixyPipe {
	public GraphTraversal pixyStep(GraphTraversal inputPipe);
	
	public void visit(PipeVisitor visitor);
} public class VariableGenerator {
	private int counter;
	private int markCounter;
	
	public VariableGenerator() {
		this.counter = 1;
	}
	
	public String newVariable(String varName) {
		if (varName.startsWith("__pixy")) {
			// Remove trailing number
			varName = varName.replaceAll("_[0-9]+$", "");
			return varName + "_" + (counter++);
		} else {
			return "__pixy_" + varName + "_" + (counter++);
		}
	}

	public void mark() {
		markCounter = counter;
	}

	public void reset() {
		counter = markCounter;
	}
} public class ConnectPipe implements PixyPipe, InternalLookupPipe {
	PixyPipe fromPipe;
	PixyPipe toPipe;
	
	public ConnectPipe(PixyPipe fromPipe, PixyPipe toPipe) {
		assert(fromPipe != null);
		assert(toPipe != null);

		this.fromPipe = fromPipe;
		this.toPipe = toPipe;
	}
	
	public String toString() {
		String lhs = fromPipe.toString();
		String rhs = toPipe.toString();
		if ((lhs.length() > 0) & (rhs.length() > 0)) {
			return lhs + " -> " + rhs;
		} else {
			return lhs + rhs;
		}
	}

	@Override
	public GraphTraversal pixyStep(GraphTraversal inputPipe) {
		GraphTraversal ans = inputPipe;
		
		ans = fromPipe.pixyStep(ans);
		ans = toPipe.pixyStep(ans);
		
		return ans;
	}

	@Override
	public Set<String> getDependentNamedSteps() {
		Set<String> ans = new HashSet<String>();
		
		PipeUtils.addDependentNamedSteps(ans, fromPipe);
		PipeUtils.addDependentNamedSteps(ans, toPipe);
		
		return ans;
	}

	@Override
	public void visit(PipeVisitor visitor) {
		fromPipe.visit(visitor);
		toPipe.visit(visitor);
	}
} public class FilterPipe implements PixyPipe, NamedInputPipe, InternalLookupPipe {
	private String namedStep;
	private PixyDatum value;

	public FilterPipe(String namedStep, PixyDatum value) {
		this.namedStep = namedStep;
		this.value = value;
		
		assert value.isFixed() || value.isAtomAPipeVar();
	}
	
	public String toString() {
 		return "pixyEval($" + ((namedStep == null) ? "" : namedStep) + " ifeq " + value + ")";
	}

	@Override
	public GraphTraversal pixyStep(GraphTraversal inputPipe) {
		GraphTraversal ans = inputPipe;
		
		PixyDatum[] ops = new PixyDatum[] {
				new PixyDatum(PixyDatumType.SPECIAL_ATOM, "$" + ((namedStep == null) ? "" : namedStep)),
				value,
				new PixyDatum(PixyDatumType.SPECIAL_ATOM, "ifeq/2")
		};

		ans = GraphTraversalExt.pixyEval(ans, Arrays.asList(ops));

		return ans;
	}

	@Override
	public String getInputNamedStep() {
		return namedStep;
	}

	@Override
	public void clearInputNamedStep() {
		this.namedStep = null;
	}

	@Override
	public void visit(PipeVisitor visitor) {
		visitor.visit(this);
	}
	
	@Override
	public Set<String> getDependentNamedSteps() {
		// The named step is already counted through the NamedInputPipe interface
		Set<String> ans = new HashSet<String>();

		if (value.isAtomAPipeVar()) {
			ans.add(value.getAtomVarName());
		}
		
		return ans;
	}
} public class PropertyPipe implements PixyPipe, NamedInputPipe, NamedOutputPipe {
	private String inStep;
	private String key;
	private String outStep;
	private Object defaultValue;

	public PropertyPipe(String namedStep, String key, String varName, Object defaultValue) {
		this.inStep = namedStep;
		this.key = key;
		this.outStep = varName;
		this.defaultValue = defaultValue;
	}

	public String toString() {
		return ((inStep == null) ? "" : "coalesce('" + inStep + "') -> ") 
				+ "property('" + key + "')"
				+ (defaultValue == null ? " -> filter(this != null)" : " -> transform(this == null ? " + defaultValue + " : this)")
				+ ((outStep == null) ? "" : " -> as('" + outStep + "')");
	}

	@Override
	public GraphTraversal pixyStep(GraphTraversal inputPipe) {
		GraphTraversal ans = inputPipe;

		if (inStep != null) {
			ans = GraphTraversalExt.coalesce(ans, inStep);
		}
		
		if (defaultValue == null) {
			// Without default
			ans = ans.properties(key).value().filter(new Predicate<Traverser<Property>>() {
				@Override
				public boolean test(Traverser x) {
					return x.get() != null;
				}
			});
		} else {
			// With default
			ans = ans.optional(__.properties(key)).map(new Function<Traverser, Object>() {
				@Override
				public Object apply(Traverser t) {
					Object x = t.get();
					if (x instanceof Property) {
						return ((Property)x).value();
					} else {
						return defaultValue;
					}
				}
			});
		}

		if (outStep != null) {
			ans = ans.as(outStep);
		}
		
		return ans;
	}

	@Override
	public String getInputNamedStep() {
		return inStep;
	}

	@Override
	public void clearInputNamedStep() {
		this.inStep = null;
	}

	@Override
	public String getOutputNamedStep() {
		return outStep;
	}

	@Override
	public void clearOutputNamedStep() {
		this.outStep = null;
	}

	@Override
	public void visit(PipeVisitor visitor) {
		visitor.visit(this);
	}
} public class MatchPipe extends FilterPipe implements PixyPipe, NamedInputPipe, InternalLookupPipe {
	public MatchPipe(String varName) {
		this(null, varName);
	}
	
	public MatchPipe(String varName1, String varName2) {
		super(varName1, new PixyDatum(PixyDatumType.SPECIAL_ATOM, "$" + varName2));
	}
}
import java.util.List;
import java.util.Map;
import com.lambdazen.pixy.PipeMaker;
import com.lambdazen.pixy.PixyDatum;
import com.lambdazen.pixy.PixyDatumType;
import com.lambdazen.pixy.PixyErrorCodes;
import com.lambdazen.pixy.PixyException;
import com.lambdazen.pixy.PixyPipe;
import com.lambdazen.pixy.VariableGenerator;
import com.lambdazen.pixy.pipes.ConnectPipe;
import com.lambdazen.pixy.pipes.FilterPipe;
import com.lambdazen.pixy.pipes.PropertyPipe;
import com.lambdazen.pixy.pipes.MatchPipe;
package com.lambdazen.pixy.pipemakers;



public class Property3 implements PipeMaker {
	@Override
	public String getSignature() {
		return "property/3";
	}
	
	@Override
	public PixyPipe makePipe(List<PixyDatum> bindings, Map<String, PixyDatum> replacements, VariableGenerator varGen) {
		PixyDatum element = bindings.get(0);
		PixyDatum key = bindings.get(1);
		PixyDatum value = bindings.get(2);
		
		return makePropertyPipe(replacements, element, key, value, null);
	}

	protected static PixyPipe makePropertyPipe(Map<String, PixyDatum> replacements,
			PixyDatum element, PixyDatum key, PixyDatum value, Object defaultValue) {
		if (key.getType() != PixyDatumType.STRING) {
			throw new PixyException(PixyErrorCodes.EXPECTING_A_STRING, "Encountered non-string property key: " + key);
		}
		
		if (element.isGround() && !value.isGround()) {
			if (element.isFixed()) {
				throw new PixyException(PixyErrorCodes.EXPECTING_A_PIPE, "Encountered a constant: " + element);
			}

			replacements.put(value.getVarName(), new PixyDatum(PixyDatumType.SPECIAL_ATOM, "$" + value.getVarName()));
			return new PropertyPipe(element.getAtomVarName(), key.getString(), value.getVarName(), defaultValue);
		} else if (element.isGround() && value.isGround()) {
			if (element.isFixed()) {
				throw new PixyException(PixyErrorCodes.EXPECTING_A_PIPE, "Encountered a constant: " + element);
			}

			if (value.isFixed()) {
				return new ConnectPipe(new PropertyPipe(element.getAtomVarName(), key.getString(), null, defaultValue),
						new FilterPipe(null, value));
			} else {
				return new ConnectPipe(new PropertyPipe(element.getAtomVarName(), key.getString(), null, defaultValue),
						new MatchPipe(value.getAtomVarName()));