protected function reduce($value, $inExp = false)
{
list($type) = $value;
switch ($type) {
case "exp":
list(, $op, $left, $right, $inParens) = $value;
$opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
$inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
$left = $this->reduce($left, true);
$right = $this->reduce($right, true);
// only do division in special cases
if ($opName == "div" && !$inParens && !$inExp) {
if ($left[0] != "color" && $right[0] != "color") {
return $this->expToString($value);
}
}
$left = $this->coerceForExpression($left);
$right = $this->coerceForExpression($right);
$ltype = $left[0];
$rtype = $right[0];
// this tries:
// 1. op_[op name]_[left type]_[right type]
// 2. op_[left type]_[right type] (passing the op as first arg
// 3. op_[op name]
$fn = "op_{$opName}_{$ltype}_{$rtype}";
if (is_callable(array($this, $fn)) || ($fn = "op_{$ltype}_{$rtype}") && is_callable(array($this, $fn)) && ($passOp = true) || ($fn = "op_{$opName}") && is_callable(array($this, $fn)) && ($genOp = true)) {
$unitChange = false;
if (!isset($genOp) && $left[0] == "number" && $right[0] == "number") {
if ($opName == "mod" && $right[2] != "") {
$this->throwError("Cannot modulo by a number with units: {$right['1']}{$right['2']}.");
}
$unitChange = true;
$emptyUnit = $left[2] == "" || $right[2] == "";
$targetUnit = "" != $left[2] ? $left[2] : $right[2];
if ($opName != "mul") {
$left[2] = "" != $left[2] ? $left[2] : $targetUnit;
$right[2] = "" != $right[2] ? $right[2] : $targetUnit;
}
if ($opName != "mod") {
$left = $this->normalizeNumber($left);
$right = $this->normalizeNumber($right);
}
if ($opName == "div" && !$emptyUnit && $left[2] == $right[2]) {
$targetUnit = "";
}
if ($opName == "mul") {
$left[2] = "" != $left[2] ? $left[2] : $right[2];
$right[2] = "" != $right[2] ? $right[2] : $left[2];
} elseif ($opName == "div" && $left[2] == $right[2]) {
$left[2] = "";
$right[2] = "";
}
}
$shouldEval = $inParens || $inExp;
if (isset($passOp)) {
$out = $this->{$fn}($op, $left, $right, $shouldEval);
} else {
$out = $this->{$fn}($left, $right, $shouldEval);
}
if (!is_null($out)) {
if ($unitChange && $out[0] == "number") {
$out = $this->coerceUnit($out, $targetUnit);
}
return $out;
}
}
return $this->expToString($value);
case "unary":
list(, $op, $exp, $inParens) = $value;
$inExp = $inExp || $this->shouldEval($exp);
$exp = $this->reduce($exp);
if ($exp[0] == "number") {
switch ($op) {
case "+":
return $exp;
case "-":
$exp[1] *= -1;
return $exp;
}
}
if ($op == "not") {
if ($inExp || $inParens) {
if ($exp == self::$false) {
return self::$true;
} else {
return self::$false;
}
} else {
$op = $op . " ";
}
}
return array("string", "", array($op, $exp));
case "var":
list(, $name) = $value;
return $this->reduce($this->get($name));
case "list":
foreach ($value[2] as &$item) {
$item = $this->reduce($item);
}
return $value;
case "string":
foreach ($value[2] as &$item) {
if (is_array($item)) {
$item = $this->reduce($item);
}
}
return $value;
case "interpolate":
$value[1] = $this->reduce($value[1]);
return $value;
case "fncall":
list(, $name, $argValues) = $value;
// user defined function?
$func = $this->get(self::$namespaces["function"] . $name, false);
if ($func) {
$this->pushEnv();
// set the args
if (isset($func->args)) {
$this->applyArguments($func->args, $argValues);
}
// throw away lines and children
$tmp = (object) array("lines" => array(), "children" => array());
$ret = $this->compileChildren($func->children, $tmp);
$this->popEnv();
return is_null($ret) ? self::$defaultValue : $ret;
}
// built in function
if ($this->callBuiltin($name, $argValues, $returnValue)) {
return $returnValue;
}
// need to flatten the arguments into a list
$listArgs = array();
foreach ((array) $argValues as $arg) {
if (empty($arg[0])) {
$listArgs[] = $this->reduce($arg[1]);
}
}
return array("function", $name, array("list", ",", $listArgs));
default:
return $value;
}
}