protected function reduce($value, $inExp = false)
{
list($type) = $value;
switch ($type) {
case Type::T_EXPRESSION:
list(, $op, $left, $right, $inParens) = $value;
$opName = isset(static::$operatorNames[$op]) ? static::$operatorNames[$op] : $op;
$inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
$left = $this->reduce($left, true);
if ($op !== 'and' && $op !== 'or') {
$right = $this->reduce($right, true);
}
// special case: looks like css shorthand
if ($opName == 'div' && !$inParens && !$inExp && isset($right[2]) && ($right[0] !== Type::T_NUMBER && $right[2] != '' || $right[0] === Type::T_NUMBER && !$right->unitless())) {
return $this->expToString($value);
}
$left = $this->coerceForExpression($left);
$right = $this->coerceForExpression($right);
$ltype = $left[0];
$rtype = $right[0];
$ucOpName = ucfirst($opName);
$ucLType = ucfirst($ltype);
$ucRType = ucfirst($rtype);
// 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{$ucOpName}{$ucLType}{$ucRType}";
if (is_callable([$this, $fn]) || ($fn = "op{$ucLType}{$ucRType}") && is_callable([$this, $fn]) && ($passOp = true) || ($fn = "op{$ucOpName}") && is_callable([$this, $fn]) && ($genOp = true)) {
$coerceUnit = false;
if (!isset($genOp) && $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER) {
$coerceUnit = true;
switch ($opName) {
case 'mul':
$targetUnit = $left[2];
foreach ($right[2] as $unit => $exp) {
$targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp;
}
break;
case 'div':
$targetUnit = $left[2];
foreach ($right[2] as $unit => $exp) {
$targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp;
}
break;
case 'mod':
$targetUnit = $left[2];
break;
default:
$targetUnit = $left->unitless() ? $right[2] : $left[2];
}
if (!$left->unitless() && !$right->unitless()) {
$left = $left->normalize();
$right = $right->normalize();
}
}
$shouldEval = $inParens || $inExp;
if (isset($passOp)) {
$out = $this->{$fn}($op, $left, $right, $shouldEval);
} else {
$out = $this->{$fn}($left, $right, $shouldEval);
}
if (isset($out)) {
if ($coerceUnit && $out[0] === Type::T_NUMBER) {
$out = $out->coerce($targetUnit);
}
return $out;
}
}
return $this->expToString($value);
case Type::T_UNARY:
list(, $op, $exp, $inParens) = $value;
$inExp = $inExp || $this->shouldEval($exp);
$exp = $this->reduce($exp);
if ($exp[0] === Type::T_NUMBER) {
switch ($op) {
case '+':
return new Node\Number($exp[1], $exp[2]);
case '-':
return new Node\Number(-$exp[1], $exp[2]);
}
}
if ($op === 'not') {
if ($inExp || $inParens) {
if ($exp === static::$false || $exp === static::$null) {
return static::$true;
}
return static::$false;
}
$op = $op . ' ';
}
return [Type::T_STRING, '', [$op, $exp]];
case Type::T_VARIABLE:
list(, $name) = $value;
return $this->reduce($this->get($name));
case Type::T_LIST:
foreach ($value[2] as &$item) {
$item = $this->reduce($item);
}
return $value;
case Type::T_MAP:
foreach ($value[1] as &$item) {
$item = $this->reduce($item);
}
foreach ($value[2] as &$item) {
$item = $this->reduce($item);
}
return $value;
case Type::T_STRING:
foreach ($value[2] as &$item) {
if (is_array($item) || $item instanceof \ArrayAccess) {
$item = $this->reduce($item);
}
}
return $value;
case Type::T_INTERPOLATE:
$value[1] = $this->reduce($value[1]);
return $value;
case Type::T_FUNCTION_CALL:
list(, $name, $argValues) = $value;
return $this->fncall($name, $argValues);
default:
return $value;
}
}