#include "eval_apply.hpp"
#include "selector.hpp"
#include "constants.hpp"
#include "prelexer.hpp"
#include "document.hpp"
#include "error.hpp"
#include <cctype>
#include <cmath>
#include <iostream>
#include <sstream>
#include <cstdlib>
#include <cstring>

namespace Sass {
  using namespace Constants;
  using std::cerr; using std::endl;

  static void throw_eval_error(Backtrace& bt, string message, string path, size_t line)
  {
    if (!path.empty() && Prelexer::string_constant(path.c_str()))
      path = path.substr(1, path.size() - 1);

    Backtrace top(&bt, path, line, "");
    message += top.to_string();

    throw Error(Error::evaluation, path, line, message);
  }

  // Expansion function for nodes in an expansion context.
  void expand(Node expr, Node prefix, Environment& env, map<string, Function>& f_env, Node_Factory& new_Node, Context& ctx, Backtrace& bt, bool function_name, const Node content)
  {
    switch (expr.type())
    {
      case Node::root: {
        for (size_t i = 0, S = expr.size(); i < S; ++i) {
            if (i == 19) {

          expand(expr[i], prefix, env, f_env, new_Node, ctx, bt, false, content);
            }
            else {
          expand(expr[i], prefix, env, f_env, new_Node, ctx, bt, false, content);
            }
        }
      } break;

      case Node::mixin: { // mixin definition
        env[expr[0].token()] = expr;
      } break;

      case Node::function: { // function definition
        f_env[expr[0].to_string()] = Function(expr);
      } break;

      case Node::mixin_call: { // mixin invocation
        Token name(expr[0].token());
        Node args(expr[1]);
        Node this_content = expr.size() == 3 ? expr[2] : Node();
        if (!this_content.is_null()) expand(this_content, prefix, env, f_env, new_Node, ctx, bt, false, content);
        if (!env.query(name)) throw_eval_error(bt, "mixin " + name.to_string() + " is undefined", expr.path(), expr.line());
        Node mixin(env[name]);
        Backtrace here(&bt, expr.path(), expr.line(), ", in mixin '" + name.to_string() + "'");
        Node expansion(apply_mixin(mixin, args, this_content, prefix, env, f_env, new_Node, ctx, here));
        expr.pop_all();   // pop the mixin metadata
        expr += expansion; // push the expansion
      } break;

      case Node::mixin_content: {
        if (!content.is_null()) {
          expr += new_Node(content);
          for (size_t i = 0, S = expr.size(); i < S; ++i) {
            re_expand(expr[i], prefix, env, f_env, new_Node, ctx, bt, false, content);
          }
        }
      } break;

      case Node::propset: {
        // TO DO: perform the property expansion here, rather than in the emitter (also requires the parser to allow interpolants in the property names)
        expr[0] = eval(expr[0], prefix, env, f_env, new_Node, ctx, bt);
        expand(expr[1], prefix, env, f_env, new_Node, ctx, bt, false, content);

        // Node block(expr[1]);
        // for (size_t i = 0, S = block.size(); i < S; ++i) {
        //   Node stm(block[i]);
        //   switch (stm.type())
        //   {
        //     case Node::propset:
        //     case Node::mixin_call:
        //     case Node::if_directive:
        //     case Node::for_through_directive:
        //     case Node::for_to_directive:
        //     case Node::each_directive:
        //     case Node::while_directive:
        //     case Node::warning: {
        //       expand(stm, prefix, env, f_env, new_Node, ctx, bt, false, content);
        //     } break;
        //   }
        // }
      } break;

      case Node::ruleset: {
        // if the selector contains interpolants, eval it and re-parse
        if (!expr[0].is_null() && expr[0].type() == Node::selector_schema) {
          Node schema(expr[0]);
          string expansion;
          for (size_t i = 0, S = schema.size(); i < S; ++i) {
            schema[i] = eval(schema[i], prefix, env, f_env, new_Node, ctx, bt);
            if (schema[i].type() == Node::string_constant) {
              expansion += schema[i].token().unquote();
            }
            else {
              expansion += schema[i].to_string();
            }
          }
          // need to re-parse the selector because its structure may have changed
          expansion += " {"; // the parser looks for an lbrace to end a selector
          char* expn_src = new char[expansion.size() + 1];
          strcpy(expn_src, expansion.c_str());
          Document needs_reparsing(Document::make_from_source_chars(ctx, expn_src, schema.path(), true));
          needs_reparsing.line = schema.line(); // set the line number to the original node's line
          expr[0] = needs_reparsing.parse_selector_group();
        }

        // Expand the selector with the prefix and save it in expr[2].
        expr << expand_selector(expr[0], prefix, new_Node);

        expand(expr[1], expr.back(), env, f_env, new_Node, ctx, bt, false, content);
      } break;

      case Node::media_query: {
        Node id(expr[0]);
        if( id.is_schema() || id.should_eval()) {
          expr[0] = eval(id, prefix, env, f_env, new_Node, ctx, bt);
        } else {
          expand(expr[0], prefix, env, f_env, new_Node, ctx, bt, false, content);
        }
        expand(expr[1], prefix, env, f_env, new_Node, ctx, bt, false, content);
        expr << prefix;
      } break;

      case Node::media_expression_group: {
        for (size_t i = 0, S = expr.size(); i < S; ++i) {
          expand(expr[i], prefix, env, f_env, new_Node, ctx, bt, false, content);
        }
      } break;

      case Node::media_expression: {
        for (size_t i = 0, S = expr.size(); i < S; ++i) {
          if (expr[i].type() == Node::rule) {
            expand(expr[i], prefix, env, f_env, new_Node, ctx, bt, false, content);
          }
          else {
            expr[i] = eval(expr[i], prefix, env, f_env, new_Node, ctx, bt);
          }
        }
      } break;

      case Node::keyframes: {
        Node id(expr[1]);
        if (id.is_schema() || id.should_eval()) {
          expr[1] = eval(id, prefix, env, f_env, new_Node, ctx, bt);
        }
        expand(expr[2], prefix, env, f_env, new_Node, ctx, bt, false, content);
      } break;

      case Node::keyframe: {
        expand(expr[0], prefix, env, f_env, new_Node, ctx, bt, false, content);
        expand(expr[1], prefix, env, f_env, new_Node, ctx, bt, false, content);
      } break;

      case Node::block: {
        Environment new_frame;
        new_frame.link(env);
        for (size_t i = 0, S = expr.size(); i < S; ++i) {
            if (i == 8) {
                expand(expr[i], prefix, new_frame, f_env, new_Node, ctx, bt, false, content);
            } else {
                expand(expr[i], prefix, new_frame, f_env, new_Node, ctx, bt, false, content);
            }
        }
      } break;

      case Node::assignment: {
        Node var(expr[0]);
        if (expr.is_guarded() && env.query(var.token())) return;
        Node val(expr[1]);
        if (val.type() == Node::list) {
          for (size_t i = 0, S = val.size(); i < S; ++i) {
            if (val[i].should_eval()) val[i] = eval(val[i], prefix, env, f_env, new_Node, ctx, bt);
          }
        }
        else {
          val = eval(val, prefix, env, f_env, new_Node, ctx, bt);
        }

        // If a binding exists (possibly upframe), then update it.
        // Otherwise, make a new on in the current frame.
        if (env.query(var.token())) {
          env[var.token()] = val;
        }
        else {
          env.current_frame[var.token()] = val;
        }
      } break;

      case Node::rule: {
        Node lhs(expr[0]);
        if (lhs.is_schema()) {
          expr[0] = eval(lhs, prefix, env, f_env, new_Node, ctx, bt);
        }
        Node rhs(expr[1]);
        if (rhs.type() == Node::list) {
          for (size_t i = 0, S = rhs.size(); i < S; ++i) {
            if (rhs[i].should_eval()) {
              rhs[i] = eval(rhs[i], prefix, env, f_env, new_Node, ctx, bt);
            }
          }
        }
        else if (rhs.is_schema() || rhs.should_eval()) {
          expr[1] = eval(rhs, prefix, env, f_env, new_Node, ctx, bt);
        }
        if (expr[1].type() == Node::list && expr[1].empty()) {
          throw_eval_error(bt, "the value of a style property may not be empty", expr[1].path(), expr[1].line());
        }
      } break;

      case Node::extend_directive: {
        if (prefix.is_null()) throw_eval_error(bt, "@extend directive may only be used within rules", expr.path(), expr.line());

        // if the extendee contains interpolants, eval it and re-parse
        if (expr[0].type() == Node::selector_schema) {
          Node schema(expr[0]);
          string expansion;
          for (size_t i = 0, S = schema.size(); i < S; ++i) {
            schema[i] = eval(schema[i], prefix, env, f_env, new_Node, ctx, bt);
            if (schema[i].type() == Node::string_constant) {
              expansion += schema[i].token().unquote();
            }
            else {
              expansion += schema[i].to_string();
            }
          }
          // need to re-parse the selector because its structure may have changed
          expansion += " {"; // the parser looks for an lbrace to end a selector
          char* expn_src = new char[expansion.size() + 1];
          strcpy(expn_src, expansion.c_str());
          Document needs_reparsing(Document::make_from_source_chars(ctx, expn_src, schema.path(), true));
          needs_reparsing.line = schema.line(); // set the line number to the original node's line
          expr[0] = needs_reparsing.parse_selector_group();
        }

        // only simple selector sequences may be extended
        switch (expr[0].type())
        {
          case Node::selector_group:
            throw_eval_error(bt, "selector groups may not be extended", expr[0].path(), expr[0].line());
            break;
          case Node::selector:
            throw_eval_error(bt, "nested selectors may not be extended", expr[0].path(), expr[0].line());
            break;
          default:
            break;
        }

        // each extendee maps to a set of extenders: extendee -> { extenders }

        // if it's a single selector, just add it to the set
        if (prefix.type() != Node::selector_group) {
          ctx.extensions.insert(pair<Node, Node>(expr[0], prefix));
        }
        // otherwise add each member of the selector group separately
        else {
          for (size_t i = 0, S = prefix.size(); i < S; ++i) {
            ctx.extensions.insert(pair<Node, Node>(expr[0], prefix[i]));
          }
        }
        ctx.has_extensions = true;
      } break;

      case Node::if_directive: {
        Node expansion = Node();
        for (size_t i = 0, S = expr.size(); i < S; i += 2) {
          if (expr[i].type() != Node::block) {
            Node predicate_val(eval(expr[i], prefix, env, f_env, new_Node, ctx, bt));
            if (!predicate_val.is_false()) {
              expand(expansion = expr[i+1], prefix, env, f_env, new_Node, ctx, bt, false, content);
              break;
            }
          }
          else {
            expand(expansion = expr[i], prefix, env, f_env, new_Node, ctx, bt, false, content);
            break;
          }
        }
        expr.pop_all();
        if (!expansion.is_null()) expr += expansion;
      } break;

      case Node::for_through_directive:
      case Node::for_to_directive: {
        Node fake_mixin(new_Node(Node::mixin, expr.path(), expr.line(), 3));
        Node fake_param(new_Node(Node::parameters, expr.path(), expr.line(), 1));
        fake_mixin << new_Node(Node::identifier, "", 0, Token::make(for_kwd)) // stub name for debugging
                   << (fake_param << expr[0])                                 // iteration variable
                   << expr[3];                                                // body
        Node lower_bound(eval(expr[1], prefix, env, f_env, new_Node, ctx, bt));
        Node upper_bound(eval(expr[2], prefix, env, f_env, new_Node, ctx, bt));
        if (!(lower_bound.is_numeric() && upper_bound.is_numeric())) {
          throw_eval_error(bt, "bounds of @for directive must be numeric", expr.path(), expr.line());
        }
        expr.pop_all();
        for (double i = lower_bound.numeric_value(),
                    U = upper_bound.numeric_value() + ((expr.type() == Node::for_to_directive) ? 0 : 1);
             i < U;
             ++i) {
          Node i_node(new_Node(expr.path(), expr.line(), i));
          Node fake_arg(new_Node(Node::arguments, expr.path(), expr.line(), 1));
          fake_arg << i_node;
          expr += apply_mixin(fake_mixin, fake_arg, content, prefix, env, f_env, new_Node, ctx, bt, true);
        }
      } break;

      case Node::each_directive: {
        Node fake_mixin(new_Node(Node::mixin, expr.path(), expr.line(), 3));
        Node fake_param(new_Node(Node::parameters, expr.path(), expr.line(), 1));
        fake_mixin << new_Node(Node::identifier, "", 0, Token::make(each_kwd)) // stub name for debugging
                   << (fake_param << expr[0])                                  // iteration variable
                   << expr[2];                                                 // body
        Node list(eval(expr[1], prefix, env, f_env, new_Node, ctx, bt));
        // If the list isn't really a list, make a singleton out of it.
        if (list.type() != Node::list) {
          list = (new_Node(Node::list, list.path(), list.line(), 1) << list);
        }
        expr.pop_all();
        for (size_t i = 0, S = list.size(); i < S; ++i) {
          Node fake_arg(new_Node(Node::arguments, expr.path(), expr.line(), 1));
          list[i].should_eval() = true;
          fake_arg << eval(list[i], prefix, env, f_env, new_Node, ctx, bt);
          expr += apply_mixin(fake_mixin, fake_arg, content, prefix, env, f_env, new_Node, ctx, bt, true);
        }
      } break;

      case Node::while_directive: {
        Node fake_mixin(new_Node(Node::mixin, expr.path(), expr.line(), 3));
        Node fake_param(new_Node(Node::parameters, expr.path(), expr.line(), 0));
        Node fake_arg(new_Node(Node::arguments, expr.path(), expr.line(), 0));
        fake_mixin << new_Node(Node::identifier, "", 0, Token::make(while_kwd))  // stub name for debugging
                   << fake_param                                                 // no iteration variable
                   << expr[1];                                                   // body
        Node pred(expr[0]);
        expr.pop_back();
        expr.pop_back();
        Node ev_pred(eval(pred, prefix, env, f_env, new_Node, ctx, bt));
        while (!ev_pred.is_false()) {
          expr += apply_mixin(fake_mixin, fake_arg, content, prefix, env, f_env, new_Node, ctx, bt, true);
          ev_pred = eval(pred, prefix, env, f_env, new_Node, ctx, bt);
        }
      } break;

      case Node::block_directive: {
        // TO DO: eval the directive name for interpolants
        expand(expr[1], new_Node(Node::none, expr.path(), expr.line(), 0), env, f_env, new_Node, ctx, bt, false, content);
      } break;

      case Node::warning: {
        Node contents(eval(expr[0], Node(), env, f_env, new_Node, ctx, bt));

        string prefix("WARNING: ");
        string indent("         ");
        string result(contents.to_string());
        if (contents.type() == Node::string_constant || contents.type() == Node::string_schema) {
          result = result.substr(1, result.size()-2); // unquote if it's a single string
        }
        // These cerrs aren't log lines! They're supposed to be here!
        cerr << prefix << result;
        // cerr << indent << "on line " << expr.line() << " of " << expr.path();
        Backtrace top(&bt, expr.path(), expr.line(), "");
        cerr << top.to_string(true);
        cerr << endl << endl;
      } break;

      default: {
        // do nothing
      } break;

    }
  }

  void re_expand(Node expr, Node prefix, Environment& env, map<string, Function>& f_env, Node_Factory& new_Node, Context& ctx, Backtrace& bt, bool function_name, const Node content)
  {
    switch (expr.type())
    {
      case Node::ruleset: {
        expr[2] = expand_selector(expr[0], prefix, new_Node);
        re_expand(expr[1], expr[2], env, f_env, new_Node, ctx, bt, false, content);
      } break;

      case Node::block:
      case Node::mixin_call:
      case Node::mixin_content:
      case Node::if_directive:
      case Node::for_through_directive:
      case Node::for_to_directive:
      case Node::each_directive:
      case Node::while_directive: {
        for (size_t i = 0, S = expr.size(); i < S; ++i) {
          re_expand(expr[i], prefix, env, f_env, new_Node, ctx, bt, false, content);
        }
      } break;

      case Node::media_query: {
        expr[2] = prefix;
        re_expand(expr[1], prefix, env, f_env, new_Node, ctx, bt, false, content);
      } break;

      case Node::block_directive: {
        re_expand(expr[1], prefix, env, f_env, new_Node, ctx, bt, false, content);
      } break;

      default: {
        return;
      } break;
    }
    return;
  }

  void expand_list(Node list, Node prefix, Environment& env, map<string, Function>& f_env, Node_Factory& new_Node, Context& ctx, Backtrace& bt)
  {
    for (size_t i = 0, S = list.size(); i < S; ++i) {
      list[i].should_eval() = true;
      list[i] = eval(list[i], prefix, env, f_env, new_Node, ctx, bt);
    }
  }

  // Evaluation function for nodes in a value context.
  Node eval(Node expr, Node prefix, Environment& env, map<string, Function>& f_env, Node_Factory& new_Node, Context& ctx, Backtrace& bt, bool function_name)
  {
    Node result = Node();
    switch (expr.type())
    {
      case Node::list: {
        // cerr << string(bt.depth(), '\t') << "about to eval a list: " << expr.to_string() << "::" << expr.is_arglist() << endl;
        bool is_arglist = expr.is_arglist();
        bool is_splat = expr.is_splat();
        if (expr.should_eval() && expr.size() > 0) {
          result = new_Node(Node::list, expr.path(), expr.line(), expr.size());
          result.is_comma_separated() = expr.is_comma_separated();
          for(size_t i = 0, S = expr.size(); i < S; i++) {
            result << eval(expr[i], prefix, env, f_env, new_Node, ctx, bt);
          }
          result.is_arglist() = is_arglist;
          result.is_splat() = is_splat;
          // cerr << string(bt.depth(), '\t') << "evaluated a list: " << result.to_string() << "::" << result.is_arglist() << endl;
        }
        // else cerr << string(bt.depth(), '\t') << "evaluated a list (sort of): " << expr.to_string() << "::" << expr.is_arglist() << endl;
      } break;

      case Node::disjunction: {
        for (size_t i = 0, S = expr.size(); i < S; ++i) {
          result = eval(expr[i], prefix, env, f_env, new_Node, ctx, bt);
          if (result.is_false()) continue;
          else                   break;
        }
      } break;

      case Node::conjunction: {
        for (size_t i = 0, S = expr.size(); i < S; ++i) {
          result = eval(expr[i], prefix, env, f_env, new_Node, ctx, bt);
          if (result.is_false()) break;
        }
      } break;

      case Node::relation: {
        Node lhs(new_Node(Node::arguments, expr[0].path(), expr[0].line(), 1));
        Node rhs(new_Node(Node::arguments, expr[2].path(), expr[2].line(), 1));
        Node rel(expr[1]);

        lhs << expr[0];
        rhs << expr[2];
        lhs = eval_arguments(lhs, prefix, env, f_env, new_Node, ctx, bt);
        rhs = eval_arguments(rhs, prefix, env, f_env, new_Node, ctx, bt);
        lhs = lhs[0];
        rhs = rhs[0];
        if (lhs.type() == Node::list) expand_list(lhs, prefix, env, f_env, new_Node, ctx, bt);
        if (rhs.type() == Node::list) expand_list(rhs, prefix, env, f_env, new_Node, ctx, bt);

        Node T(new_Node(Node::boolean, lhs.path(), lhs.line(), true));
        Node F(new_Node(Node::boolean, lhs.path(), lhs.line(), false));

        switch (rel.type())
        {
          case Node::eq:  result = ((lhs == rhs) ? T : F); break;
          case Node::neq: result = ((lhs != rhs) ? T : F); break;
          case Node::gt:  result = ((lhs > rhs)  ? T : F); break;
          case Node::gte: result = ((lhs >= rhs) ? T : F); break;
          case Node::lt:  result = ((lhs < rhs)  ? T : F); break;
          case Node::lte: result = ((lhs <= rhs) ? T : F); break;
          default:
            throw_eval_error(bt, "unknown comparison operator " + expr.token().to_string(), expr.path(), expr.line());
        }
      } break;

      case Node::expression: {
        Node list(new_Node(Node::expression, expr.path(), expr.line(), expr.size()));
        for (size_t i = 0, S = expr.size(); i < S; ++i) {
          list << eval(expr[i], prefix, env, f_env, new_Node, ctx, bt);
        }
        result = reduce(list, 1, list[0], new_Node, bt);
      } break;

      case Node::term: {
        if (expr.should_eval()) {
          Node list(new_Node(Node::term, expr.path(), expr.line(), expr.size()));
          for (size_t i = 0, S = expr.size(); i < S; ++i) {
            list << eval(expr[i], prefix, env, f_env, new_Node, ctx, bt);
          }
          result = reduce(list, 1, list[0], new_Node, bt);
        }
      } break;

      case Node::textual_percentage: {
        result = new_Node(expr.path(), expr.line(), std::atof(expr.token().begin), Node::numeric_percentage);
      } break;

      case Node::textual_dimension: {
        result = new_Node(expr.path(), expr.line(),
                          std::atof(expr.token().begin),
                          Token::make(Prelexer::number(expr.token().begin),
                                      expr.token().end));
      } break;

      case Node::textual_number: {
        result = new_Node(expr.path(), expr.line(), std::atof(expr.token().begin));
      } break;

      case Node::textual_hex: {
        result = new_Node(Node::numeric_color, expr.path(), expr.line(), 4);
        Token hext(Token::make(expr.token().begin+1, expr.token().end));
        if (hext.length() == 6) {
          for (int i = 0; i < 6; i += 2) {
            result << new_Node(expr.path(), expr.line(), static_cast<double>(std::strtol(string(hext.begin+i, 2).c_str(), NULL, 16)));
          }
        }
        else {
          for (int i = 0; i < 3; ++i) {
            result << new_Node(expr.path(), expr.line(), static_cast<double>(std::strtol(string(2, hext.begin[i]).c_str(), NULL, 16)));
          }
        }
        result << new_Node(expr.path(), expr.line(), 1.0);
      } break;

      case Node::variable: {
        if (!env.query(expr.token())) throw_eval_error(bt, "reference to unbound variable " + expr.token().to_string(), expr.path(), expr.line());
        result = env[expr.token()];
        // cerr << string(bt.depth(), '\t') << "evaluated a lookup: " << expr.to_string() << " -> " << result.to_string() << "::" << result.is_arglist() << endl;
      } break;

      case Node::uri: {
        result = new_Node(Node::uri, expr.path(), expr.line(), 1);
        result << eval(expr[0], prefix, env, f_env, new_Node, ctx, bt);
      } break;

      case Node::function_call: {
        // TO DO: default-constructed Function should be a generic callback (maybe)

        // eval the function name in case it's interpolated
        Node name_node(eval(expr[0], prefix, env, f_env, new_Node, ctx, bt, true));
        string name(name_node.to_string());
        if (!f_env.count(name)) {
          // no definition available; just pass it through (with evaluated args)
          Node args(expr[1]);
          Node evaluated_args(new_Node(Node::arguments, args.path(), args.line(), args.size()));
          for (size_t i = 0, S = args.size(); i < S; ++i) {
            evaluated_args << eval(args[i], prefix, env, f_env, new_Node, ctx, bt);
            if (evaluated_args.back().type() == Node::list) {
              Node arg_list(evaluated_args.back());
              for (size_t j = 0, S = arg_list.size(); j < S; ++j) {
                if (arg_list[j].should_eval()) arg_list[j] = eval(arg_list[j], prefix, env, f_env, new_Node, ctx, bt);
              }
            }
          }
          result = new_Node(Node::function_call, expr.path(), expr.line(), 2);
          result << name_node << evaluated_args;
        }
        else {
          // check to see if the function is an overloaded primitive
          Function f(f_env[name]);
          if (f.overloaded) {
            stringstream s;
            s << name << " " << expr[1].size();
            string resolved_name(s.str());
            if (!f_env.count(resolved_name)) throw_eval_error(bt, "wrong number of arguments to " + name, expr.path(), expr.line());
            f = f_env[resolved_name];
          }
          Backtrace here(&bt, expr.path(), expr.line(), ", in function '" + name + "'");
          result = apply_function(f, expr[1], prefix, env, f_env, new_Node, ctx, here, expr.path(), expr.line());
        }
      } break;

      case Node::unary_plus: {
        Node arg(eval(expr[0], prefix, env, f_env, new_Node, ctx, bt));
        if (arg.is_numeric()) {
          result = arg;
        }
        else {
          result = new_Node(Node::unary_plus, expr.path(), expr.line(), 1);
          result << arg;
        }
      } break;

      case Node::unary_minus: {
        Node arg(eval(expr[0], prefix, env, f_env, new_Node, ctx, bt));
        if (arg.is_numeric()) {
          double neg_val = -arg.numeric_value();
          Node::Type t = arg.type();
          switch (t)
          {
            case Node::number:
            case Node::numeric_percentage: {
              result = new_Node(expr.path(), expr.line(), neg_val, t);
            } break;

            case Node::numeric_dimension: {
              result = new_Node(expr.path(), expr.line(), neg_val, arg.unit());
            } break;

            default: {
              // unreachable
              result = arg;
            } break;
          }
        }
        else {
          result = new_Node(Node::unary_minus, expr.path(), expr.line(), 1);
          result << arg;
        }
      } break;

      case Node::identifier: {
        string id_str(expr.to_string());
        to_lowercase(id_str);
        if (!function_name && ctx.color_names_to_values.count(id_str)) {
          Node color_orig(ctx.color_names_to_values[id_str]);
          Node r(color_orig[0]);
          Node g(color_orig[1]);
          Node b(color_orig[2]);
          Node a(color_orig[3]);
          result = new_Node(expr.path(), expr.line(),
                            r.numeric_value(),
                            g.numeric_value(),
                            b.numeric_value(),
                            a.numeric_value());
        }
      } break;

      case Node::string_schema:
      case Node::value_schema:
      case Node::identifier_schema: {
        result = new_Node(expr.type(), expr.path(), expr.line(), expr.size());
        for (size_t i = 0, S = expr.size(); i < S; ++i) {
          result << eval(expr[i], prefix, env, f_env, new_Node, ctx, bt);
        }
        result.is_quoted() = expr.is_quoted();
      } break;

      case Node::css_import: {
        result = new_Node(Node::css_import, expr.path(), expr.line(), 1);
        result << eval(expr[0], prefix, env, f_env, new_Node, ctx, bt);
      } break;

      default: {
        result = expr;
      } break;
    }
    if (result.is_null()) result = expr;
    return result;
  }

  // Reduce arithmetic operations. Arithmetic expressions are stored as vectors
  // of operands with operators interspersed, rather than as the usual binary
  // tree. (This function is essentially a left fold.)
  Node reduce(Node list, size_t head, Node acc, Node_Factory& new_Node, Backtrace& bt)
  {
    if (head >= list.size()) return acc;
    Node op(list[head]);
    Node rhs(list[head + 1]);
    Node::Type optype = op.type();
    Node::Type ltype = acc.type();
    Node::Type rtype = rhs.type();
    if (ltype == Node::number && rtype == Node::number) {
      acc = new_Node(list.path(), list.line(), operate(op, acc.numeric_value(), rhs.numeric_value(), bt));
    }
    else if (ltype == Node::number && rtype == Node::numeric_dimension) {
      acc = new_Node(list.path(), list.line(), operate(op, acc.numeric_value(), rhs.numeric_value(), bt), rhs.unit());
    }
    else if (ltype == Node::number && rtype == Node::numeric_percentage) {
      acc = new_Node(list.path(), list.line(), operate(op, acc.numeric_value(), rhs.numeric_value(), bt), Node::numeric_percentage);
    }
    else if (ltype == Node::numeric_dimension && rtype == Node::number) {
      acc = new_Node(list.path(), list.line(), operate(op, acc.numeric_value(), rhs.numeric_value(), bt), acc.unit());
    }
    else if (ltype == Node::numeric_percentage && rtype == Node::number) {
      acc = new_Node(list.path(), list.line(), operate(op, acc.numeric_value(), rhs.numeric_value(), bt), Node::numeric_percentage);
    }
    else if (ltype == Node::numeric_dimension && rtype == Node::numeric_dimension) {
      // TO DO: TRUE UNIT ARITHMETIC
      if (optype == Node::div) {
        acc = new_Node(list.path(), list.line(), operate(op, acc.numeric_value(), rhs.numeric_value(), bt));
      }
      else {
        acc = new_Node(list.path(), list.line(), operate(op, acc.numeric_value(), rhs.numeric_value(), bt), acc.unit());
      }
    }
    else if (ltype == Node::numeric_percentage && rtype == Node::numeric_percentage) {
      // TO DO: TRUE UNIT ARITHMETIC
      if (optype == Node::div) {
        acc = new_Node(list.path(), list.line(), operate(op, acc.numeric_value(), rhs.numeric_value(), bt));
      }
      else {
        acc = new_Node(list.path(), list.line(), operate(op, acc.numeric_value(), rhs.numeric_value(), bt), Node::numeric_percentage);
      }
    }
    else if (ltype == Node::number && rtype == Node::numeric_color) {
      if (optype == Node::add || optype == Node::mul) {
        double r = operate(op, acc.numeric_value(), rhs[0].numeric_value(), bt);
        double g = operate(op, acc.numeric_value(), rhs[1].numeric_value(), bt);
        double b = operate(op, acc.numeric_value(), rhs[2].numeric_value(), bt);
        double a = rhs[3].numeric_value();
        acc = new_Node(list.path(), list.line(), r, g, b, a);
      }
      else {
        acc = (new_Node(Node::value_schema, list.path(), list.line(), 3) << acc);
        acc << op;
        acc << rhs;
      }
    }
    else if (ltype == Node::numeric_color && rtype == Node::number) {
      double r = operate(op, acc[0].numeric_value(), rhs.numeric_value(), bt);
      double g = operate(op, acc[1].numeric_value(), rhs.numeric_value(), bt);
      double b = operate(op, acc[2].numeric_value(), rhs.numeric_value(), bt);
      double a = acc[3].numeric_value();
      acc = new_Node(list.path(), list.line(), r, g, b, a);
    }
    else if (ltype == Node::numeric_color && rtype == Node::numeric_color) {
      if (acc[3].numeric_value() != rhs[3].numeric_value()) throw_eval_error(bt, "alpha channels must be equal for " + acc.to_string() + " + " + rhs.to_string(), acc.path(), acc.line());
      double r = operate(op, acc[0].numeric_value(), rhs[0].numeric_value(), bt);
      double g = operate(op, acc[1].numeric_value(), rhs[1].numeric_value(), bt);
      double b = operate(op, acc[2].numeric_value(), rhs[2].numeric_value(), bt);
      double a = acc[3].numeric_value();
      acc = new_Node(list.path(), list.line(), r, g, b, a);
    }
    else if (ltype == Node::concatenation && rtype == Node::concatenation) {
      if (optype != Node::add) acc << op;
      acc += rhs;
    }
    else if (ltype == Node::concatenation) {
      if (optype != Node::add) acc << op;
      acc << rhs;
    }
    else if (rtype == Node::concatenation) {
      acc = (new_Node(Node::concatenation, list.path(), list.line(), 2) << acc);
      if (optype != Node::add) acc << op;
      acc += rhs;
      acc.is_quoted() = acc[0].is_quoted();
    }
    else if (acc.is_string() || rhs.is_string()) {
      acc = (new_Node(Node::concatenation, list.path(), list.line(), 2) << acc);
      if (optype != Node::add) acc << op;
      acc << rhs;
      if (acc[0].is_quoted() || (ltype == Node::number && rhs.is_quoted())) {
        acc.is_quoted() = true;
      }
      else {
        acc.is_quoted() = false;
      }
    }
    else { // lists or schemas
      if (acc.is_schema() && rhs.is_schema()) {
        if (optype != Node::add) acc << op;
        acc += rhs;
      }
      else if (acc.is_schema()) {
        if (optype != Node::add) acc << op;
        acc << rhs;
      }
      else if (rhs.is_schema()) {
        acc = (new_Node(Node::value_schema, list.path(), list.line(), 2) << acc);
        if (optype != Node::add) acc << op;
        acc += rhs;
      }
      else {
        acc = (new_Node(Node::value_schema, list.path(), list.line(), 2) << acc);
        if (optype != Node::add) acc << op;
        acc << rhs;
      }
      acc.is_quoted() = false;
    }
    return reduce(list, head + 2, acc, new_Node, bt);
  }

  // Helper for doing the actual arithmetic.
  double operate(Node op, double lhs, double rhs, Backtrace& bt)
  {
    switch (op.type())
    {
      case Node::add: return lhs + rhs; break;
      case Node::sub: return lhs - rhs; break;
      case Node::mul: return lhs * rhs; break;
      case Node::div: {
        if (rhs == 0) throw_eval_error(bt, "divide by zero", op.path(), op.line());
        return lhs / rhs;
      } break;
      case Node::mod: {
        if (rhs == 0) throw_eval_error(bt, "divide by zero", op.path(), op.line());
        return std::fmod(lhs, rhs);
      } break;
      default:        return 0;         break;
    }
  }

  Node eval_arguments(Node args, Node prefix, Environment& env, map<string, Function>& f_env, Node_Factory& new_Node, Context& ctx, Backtrace& bt)
  {
    // cerr << string(bt.depth(), '\t') << "evaluating arguments" << endl;
    Node evaluated_args(new_Node(Node::arguments, args.path(), args.line(), args.size()));
    for (size_t i = 0, S = args.size(); i < S; ++i) {
      if (args[i].type() != Node::assignment) {
        // bool is_arglist = args[i].is_arglist();
        bool is_splat = args[i].is_splat();
        evaluated_args << eval(args[i], prefix, env, f_env, new_Node, ctx, bt);
        // cerr << string(bt.depth(), '\t') << "just pushed an evaluated arg: " << evaluated_args[i].to_string() << "::" << evaluated_args[i].is_arglist() << endl;
        // if (evaluated_args[i].type() == Node::list) {
        //   Node arg_list(evaluated_args[i]);
        //   Node new_arg_list(new_Node(Node::list, arg_list.path(), arg_list.line(), arg_list.size()));
        //   new_arg_list.is_arglist() = arg_list.is_arglist();
        //   for (size_t j = 0, S = arg_list.size(); j < S; ++j) {
        //     if (arg_list[j].should_eval()) new_arg_list << eval(arg_list[j], prefix, env, f_env, new_Node, ctx, bt);
        //     else                           new_arg_list << arg_list[j];
        //   }
        //   evaluated_args[i] = new_arg_list;
        // }
        // evaluated_args[i].is_arglist() = is_arglist;
        // evaluated_args[i].is_arglist() = is_arglist;
        evaluated_args[i].is_splat() = is_splat;
        // cerr << string(bt.depth(), '\t') << "after first arg evaluation: " << evaluated_args[i].to_string() << "::" << evaluated_args[i].is_arglist() << endl;
      }
      else {
        // cerr << "shouldn't be taking this branch!" << endl;
        Node kwdarg(new_Node(Node::assignment, args[i].path(), args[i].line(), 2));
        kwdarg << args[i][0];
        kwdarg << eval(args[i][1], prefix, env, f_env, new_Node, ctx, bt);
        if (kwdarg.back().type() == Node::list) {
          Node arg_list(kwdarg.back());
          Node new_arg_list(new_Node(Node::list, arg_list.path(), arg_list.line(), arg_list.size()));
          for (size_t j = 0, S = arg_list.size(); j < S; ++j) {
            if (arg_list[j].should_eval()) new_arg_list << eval(arg_list[j], prefix, env, f_env, new_Node, ctx, bt);
            else                           new_arg_list << arg_list[j];
          }
          kwdarg[1] = new_arg_list;
        }
        evaluated_args << kwdarg;
      }
    }
    // cerr << string(bt.depth(), '\t') << "after first arg evaluation: " << evaluated_args.back().to_string() << "::" << evaluated_args.back().is_arglist() << endl;
    // eval twice because args may be delayed
    for (size_t i = 0, S = evaluated_args.size(); i < S; ++i) {
      if (evaluated_args[i].type() != Node::assignment) {
        // bool is_arglist = evaluated_args[i].is_arglist();
        bool is_splat = evaluated_args[i].is_splat();
        evaluated_args[i].should_eval() = true;
        evaluated_args[i] = eval(evaluated_args[i], prefix, env, f_env, new_Node, ctx, bt);
        if (evaluated_args[i].type() == Node::list) {
          Node arg_list(evaluated_args[i]);
          for (size_t j = 0, S = arg_list.size(); j < S; ++j) {
            if (arg_list[j].should_eval()) arg_list[j] = eval(arg_list[j], prefix, env, f_env, new_Node, ctx, bt);
          }
        }
        // evaluated_args[i].is_arglist() = is_arglist;
        evaluated_args[i].is_splat() = is_splat;
      }
      else {
        Node kwdarg(evaluated_args[i]);
        kwdarg[1].should_eval() = true;
        kwdarg[1] = eval(kwdarg[1], prefix, env, f_env, new_Node, ctx, bt);
        if (kwdarg[1].type() == Node::list) {
          Node arg_list(kwdarg[1]);
          for (size_t j = 0, S = arg_list.size(); j < S; ++j) {
            if (arg_list[j].should_eval()) arg_list[j] = eval(arg_list[j], prefix, env, f_env, new_Node, ctx, bt);
          }
        }
        evaluated_args[i] = kwdarg;
      }
    }
    // cerr << string(bt.depth(), '\t') << "after second arg evaluation: " << evaluated_args.back().to_string() << "::" << evaluated_args.back().is_arglist() << endl;
    return evaluated_args;
  }

  // Helper function for binding arguments in function and mixin calls.
  // Needs the environment containing the bindings to be passed in by the
  // caller. Also expects the caller to have pre-evaluated the arguments.
  void bind_arguments(string callee_name, const Node params, const Node args, Node prefix, Environment& env, map<string, Function>& f_env, Node_Factory& new_Node, Context& ctx, Backtrace& bt)
  {
    // populate the env with the names of the parameters so we can check for
    // correctness further down
    bool has_rest_params = false;
    for (size_t i = 0, S = params.size(); i < S; ++i) {
      Node param(params[i]);
      // env.current_frame[param.type() == Node::assignment ? param[0].token() : param.token()] = Node();
      if (param.type() == Node::variable) {
        env.current_frame[param.token()] = Node();
      }
      else if (param.type() == Node::assignment) {
        env.current_frame[param[0].token()] = Node();
      }
      else {
        Node arglist(new_Node(Node::list, args.path(), args.line(), 0));
        arglist.is_arglist() = true;
        arglist.is_comma_separated() = true;
        env.current_frame[param.token()] = arglist;
        has_rest_params = true;
      }
    }

    // now do the actual binding
    size_t args_bound = 0, num_params = params.size();
    for (size_t i = 0, j = 0, S = args.size(); i < S; ++i) {
      if (j == num_params-1 && has_rest_params) {
        Node arglist(env[params[j].token()]);
        // collect rest-args
        if (args[i].type() == Node::list && args[i].is_splat()) {
          arglist += args[i];
          arglist.is_comma_separated() = args[i].is_comma_separated();
        }
        else {
          for (size_t k = i; k < S; ++k) {
            arglist << args[k];
          }
        }
        break;
      }
      else if (j >= num_params) {
        stringstream msg;
        msg << callee_name << " only takes " << num_params << " arguments";
        throw_eval_error(bt, msg.str(), args.path(), args.line());
      }
      // ordinal argument; just bind it and keep going
      else if (args[i].type() != Node::assignment) {
        // if it's a splat
        if (args[i].type() == Node::list && args[i].is_splat()) {
          // loop through the splatted list and bind each element to the remaining parameters
          for (size_t rest_i = 0, rest_length = args[i].size(); rest_i < rest_length; ++rest_i) {
            // if we're binding the last parameter
            if (j == num_params-1) {
              Node leftovers;
              if (has_rest_params) {
                leftovers = env[params[j].token()];
                leftovers.is_comma_separated() = args[i].is_comma_separated();
                for (; rest_i < rest_length; ++rest_i) {
                  leftovers << args[i][rest_i];
                }
              }
              else {
                leftovers = args[i][rest_i];
              }
              Node param(params[j]);
              env[param.type() != Node::assignment ? param.token() : param[0].token()] = leftovers;
              break;
            }
            // otherwise keep going normally
            else {
              Node param(params[j]);
              Token name = ((param.type() == Node::variable) ? param.token() : param[0].token());
              env[name] = args[i][rest_i];
              ++j;
            }
          }
        }
        else {
          Node arg(args[i]), param(params[j]);
          env[param.type() == Node::variable ? param.token() : param[0].token()] = arg;
          ++j;
        }
      }
      // keyword argument -- need to check for correctness
      else {
        Node arg(args[i]), param(params[j]);
        Token arg_name(arg[0].token());
        Node arg_value(arg[1]);
        if (!env.query(arg_name)) {
          throw_eval_error(bt, callee_name + " has no parameter named " + arg_name.to_string(), arg.path(), arg.line());
        }
        if (!env[arg_name].is_null()) {
          throw_eval_error(bt, callee_name + " was passed argument " + arg_name.to_string() + " both by position and by name", arg.path(), arg.line());
        }
        env[arg_name] = arg_value;
        ++args_bound;
      }
    }
    // now plug in the holes with default values, if any
    for (size_t i = 0, S = params.size(); i < S; ++i) {
      Node param(params[i]);
      Token param_name((param.type() == Node::assignment ? param[0] : param).token());
      if (env[param_name].is_null()) {
        if (param.type() != Node::assignment) {
          throw_eval_error(bt, callee_name + " is missing argument " + param_name.to_string(), args.path(), args.line());
        }
        // eval default values in an environment where the previous vals have already been evaluated
        env[param_name] = eval(param[1], prefix, env, f_env, new_Node, ctx, bt);
      }
    }
  }

  // Apply a mixin -- bind the arguments in a new environment, link the new
  // environment to the current one, then copy the body and eval in the new
  // environment.
  Node apply_mixin(Node mixin, const Node args, const Node content, Node prefix, Environment& env, map<string, Function>& f_env, Node_Factory& new_Node, Context& ctx, Backtrace& bt, bool dynamic_scope)
  {
    stringstream mixin_name;
    mixin_name << "mixin";
    if (!mixin[0].is_null()) mixin_name << " " << mixin[0].to_string();
    // cerr << string(bt.depth(), '\t') << "applying " << mixin_name.str() << endl;
    Node params(mixin[1]);
    Node body(new_Node(mixin[2])); // clone the body
    Node evaluated_args(eval_arguments(args, prefix, env, f_env, new_Node, ctx, bt));
    // Create a new environment for the mixin and link it to the appropriate parent
    Environment bindings;
    if (dynamic_scope) {
      bindings.link(env);
    }
    else {
      // C-style scope for now (current/global, nothing in between). May need
      // to implement full lexical scope someday.
      bindings.link(env.global ? *env.global : env);
    }
    // bind arguments in the extended environment
    // stringstream mixin_name;
    // mixin_name << "mixin";
    // if (!mixin[0].is_null()) mixin_name << " " << mixin[0].to_string();
    bind_arguments(mixin_name.str(), params, evaluated_args, prefix, bindings, f_env, new_Node, ctx, bt);
    // cerr << string(bt.depth(), '\t') << "last binding: " << params.back().to_string() << " -> " << bindings[params.back().token()].to_string() << "::" << bindings[params.back().token()].is_arglist() << endl;
    // evaluate the mixin's body
    expand(body, prefix, bindings, f_env, new_Node, ctx, bt, false, content);
    return body;
  }

  // Apply a function -- bind the arguments and pass them to the underlying
  // primitive function implementation, then return its value.
  Node apply_function(const Function& f, const Node args, Node prefix, Environment& env, map<string, Function>& f_env, Node_Factory& new_Node, Context& ctx, Backtrace& bt, string& path, size_t line)
  {
    // cerr << string(bt.depth(), '\t') << "applying " << f.name << endl;
    // cerr << string(bt.depth(), '\t') << "last pre-evaluated arg: " << args.back().to_string() << endl;
    Node evaluated_args(eval_arguments(args, prefix, env, f_env, new_Node, ctx, bt));
    // cerr << string(bt.depth(), '\t') << "last evaluated arg: " << evaluated_args.back().to_string() << "::" << evaluated_args.back().is_arglist() << endl;
    // bind arguments
    Environment bindings;
    Node params(f.primitive || f.c_func ? f.parameters : f.definition[1]);
    bindings.link(env.global ? *env.global : env);
    bind_arguments("function " + f.name, params, evaluated_args, prefix, bindings, f_env, new_Node, ctx, bt);

    // cerr << string(bt.depth(), '\t') << "last binding: " << params.back().to_string() << " -> " << bindings[params.back().token()].to_string() << "::" << bindings[params.back().token()].is_arglist() << endl;

    if (f.primitive) {
      return f.primitive(f.parameter_names, bindings, new_Node, bt, path, line);
    }
    else if (f.c_func) {
      size_t num_params = f.parameter_names.size();
      union Sass_Value c_args = new_sass_c_list(num_params, SASS_COMMA);
      for (size_t i = 0; i < num_params; ++i) {
        c_args.list.values[i] = bindings[f.parameter_names[i].token()].to_c_val();
      }
      union Sass_Value c_val = f.c_func(c_args);
      if (c_val.unknown.tag == SASS_ERROR) {
        throw_eval_error(bt, "error in C function " + f.name + ": " + c_val.error.message, path, line);
      }
      Node val(c_val_to_node(c_val, ctx, bt, path, line));
      free_sass_value(c_val);
      return val;
    }
    else {
      // TO DO: consider cloning the function body?
      return eval_function(f.name, f.definition[2], bindings, new_Node, ctx, bt, true);
    }
  }

  // Special function for evaluating pure Sass functions. The evaluation
  // algorithm is different in this case because the body needs to be
  // executed and a single value needs to be returned directly, rather than
  // styles being expanded and spliced in place.
  Node eval_function(string name, Node body, Environment& bindings, Node_Factory& new_Node, Context& ctx, Backtrace& bt, bool at_toplevel)
  {
    for (size_t i = 0, S = body.size(); i < S; ++i) {
      Node stm(body[i]);
      switch (stm.type())
      {
        case Node::assignment: {
          Node val(stm[1]);
          Node newval;
          if (val.type() == Node::list) {
            newval = new_Node(Node::list, val.path(), val.line(), val.size());
            newval.is_comma_separated() = val.is_comma_separated();
            for (size_t i = 0, S = val.size(); i < S; ++i) {
              if (val[i].should_eval()) newval << eval(val[i], Node(), bindings, ctx.function_env, new_Node, ctx, bt);
              else                      newval << val[i];
            }
          }
          else {
            newval = eval(val, Node(), bindings, ctx.function_env, new_Node, ctx, bt);
          }
          Node var(stm[0]);
          if (stm.is_guarded() && bindings.query(var.token())) continue;
          // If a binding exists (possibly upframe), then update it.
          // Otherwise, make a new one in the current frame.
          if (bindings.query(var.token())) {
            bindings[var.token()] = newval;
          }
          else {
            bindings.current_frame[var.token()] = newval;
          }
        } break;

        case Node::if_directive: {
          for (size_t j = 0, S = stm.size(); j < S; j += 2) {
            if (stm[j].type() != Node::block) {
              Node predicate_val(eval(stm[j], Node(), bindings, ctx.function_env, new_Node, ctx, bt));
              if (!predicate_val.is_false()) {
                Node v(eval_function(name, stm[j+1], bindings, new_Node, ctx, bt));
                if (v.is_null()) break;
                else             return v;
              }
            }
            else {
              Node v(eval_function(name, stm[j], bindings, new_Node, ctx, bt));
              if (v.is_null()) break;
              else             return v;
            }
          }
        } break;

        case Node::for_through_directive:
        case Node::for_to_directive: {
          Node::Type for_type = stm.type();
          Node iter_var(stm[0]);
          Node lower_bound(eval(stm[1], Node(), bindings, ctx.function_env, new_Node, ctx, bt));
          Node upper_bound(eval(stm[2], Node(), bindings, ctx.function_env, new_Node, ctx, bt));
          Node for_body(stm[3]);
          Environment for_env; // re-use this env for each iteration
          for_env.link(bindings);
          for (double j = lower_bound.numeric_value(), T = upper_bound.numeric_value() + ((for_type == Node::for_to_directive) ? 0 : 1);
               j < T;
               j += 1) {
            for_env.current_frame[iter_var.token()] = new_Node(lower_bound.path(), lower_bound.line(), j);
            Node v(eval_function(name, for_body, for_env, new_Node, ctx, bt));
            if (v.is_null()) continue;
            else             return v;
          }
        } break;

        case Node::each_directive: {
          Node iter_var(stm[0]);
          Node list(eval(stm[1], Node(), bindings, ctx.function_env, new_Node, ctx, bt));
          if (list.type() != Node::list) {
            list = (new_Node(Node::list, list.path(), list.line(), 1) << list);
          }
          Node each_body(stm[2]);
          Environment each_env; // re-use this env for each iteration
          each_env.link(bindings);
          for (size_t j = 0, T = list.size(); j < T; ++j) {
            list[j].should_eval() = true;
            each_env.current_frame[iter_var.token()] = eval(list[j], Node(), bindings, ctx.function_env, new_Node, ctx, bt);
            Node v(eval_function(name, each_body, each_env, new_Node, ctx, bt));
            if (v.is_null()) continue;
            else             return v;
          }
        } break;

        case Node::while_directive: {
          Node pred_expr(stm[0]);
          Node while_body(stm[1]);
          Environment while_env; // re-use this env for each iteration
          while_env.link(bindings);
          Node pred_val(eval(pred_expr, Node(), bindings, ctx.function_env, new_Node, ctx, bt));
          while (!pred_val.is_false()) {
            Node v(eval_function(name, while_body, while_env, new_Node, ctx, bt));
            if (v.is_null()) {
              pred_val = eval(pred_expr, Node(), bindings, ctx.function_env, new_Node, ctx, bt);
              continue;
            }
            else return v;
          }
        } break;

        case Node::warning: {
          string prefix("WARNING: ");
          string indent("         ");
          Node contents(eval(stm[0], Node(), bindings, ctx.function_env, new_Node, ctx, bt));
          string result(contents.to_string());
          if (contents.type() == Node::string_constant || contents.type() == Node::string_schema) {
            result = result.substr(1, result.size()-2); // unquote if it's a single string
          }
          // These cerrs aren't log lines! They're supposed to be here!
          cerr << prefix << result;
          // cerr << indent << "on line " << stm.line() << " of " << stm.path();
          Backtrace top(&bt, stm.path(), stm.line(), "");
          cerr << top.to_string(true);
          cerr << endl << endl;
        } break;

        case Node::return_directive: {
          Node retval(eval(stm[0], Node(), bindings, ctx.function_env, new_Node, ctx, bt));
          if (retval.type() == Node::list) {
            Node new_list(new_Node(Node::list, retval.path(), retval.line(), retval.size()));
            new_list.is_comma_separated() = retval.is_comma_separated();
            for (size_t i = 0, S = retval.size(); i < S; ++i) {
              new_list << eval(retval[i], Node(), bindings, ctx.function_env, new_Node, ctx, bt);
            }
            retval = new_list;
          }
          return retval;
        } break;

        default: {

        } break;
      }
    }
    if (at_toplevel) throw_eval_error(bt, "function finished without @return", body.path(), body.line());
    return Node();
  }

  // Expand a selector with respect to its prefix/context. Two separate cases:
  // when the selector has backrefs, substitute the prefix for each occurrence
  // of a backref. When the selector doesn't have backrefs, just prepend the
  // prefix. This function needs multiple subsidiary cases in order to properly
  // combine the various kinds of selectors.
  Node expand_selector(Node sel, Node pre, Node_Factory& new_Node)
  {
    if (pre.is_null() || sel.is_null()) return sel;

    if (sel.has_backref()) {
      if ((pre.type() == Node::selector_group) && (sel.type() == Node::selector_group)) {
        Node group(new_Node(Node::selector_group, sel.path(), sel.line(), pre.size() * sel.size()));
        for (size_t i = 0, S = pre.size(); i < S; ++i) {
          for (size_t j = 0, T = sel.size(); j < T; ++j) {
            if (sel[j].has_backref()) {
              group << expand_backref(new_Node(sel[j]), pre[i]);
            }
            else {
              Node new_sel(new_Node(Node::selector, sel.path(), sel.line(), 2));
              if (pre[i].type() == Node::selector) new_sel += pre[i];
              else                                 new_sel << pre[i];
              if (sel[j].type() == Node::selector) new_sel += sel[j];
              else                                 new_sel << sel[j];
              group << new_sel;
            }
          }
        }
        return group;
      }
      else if ((pre.type() == Node::selector_group) && (sel.type() != Node::selector_group)) {
        Node group(new_Node(Node::selector_group, sel.path(), sel.line(), pre.size()));
        for (size_t i = 0, S = pre.size(); i < S; ++i) {
          group << expand_backref(new_Node(sel), pre[i]);
        }
        return group;
      }
      else if ((pre.type() != Node::selector_group) && (sel.type() == Node::selector_group)) {
        Node group(new_Node(Node::selector_group, sel.path(), sel.line(), sel.size()));
        for (size_t i = 0, S = sel.size(); i < S; ++i) {
          if (sel[i].has_backref()) {
            group << expand_backref(new_Node(sel[i]), pre);
          }
          else {
            Node new_sel(new_Node(Node::selector, sel.path(), sel.line(), 2));
            if (pre.type() == Node::selector)    new_sel += pre;
            else                                 new_sel << pre;
            if (sel[i].type() == Node::selector) new_sel += sel[i];
            else                                 new_sel << sel[i];
            group << new_sel;
          }
        }
        return group;
      }
      else {
        return expand_backref(new_Node(sel), pre);
      }
    }

    if ((pre.type() == Node::selector_group) && (sel.type() == Node::selector_group)) {
      Node group(new_Node(Node::selector_group, sel.path(), sel.line(), pre.size() * sel.size()));
      for (size_t i = 0, S = pre.size(); i < S; ++i) {
        for (size_t j = 0, T = sel.size(); j < T; ++j) {
          Node new_sel(new_Node(Node::selector, sel.path(), sel.line(), 2));
          if (pre[i].type() == Node::selector) new_sel += pre[i];
          else                                 new_sel << pre[i];
          if (sel[j].type() == Node::selector) new_sel += sel[j];
          else                                 new_sel << sel[j];
          group << new_sel;
        }
      }
      return group;
    }
    else if ((pre.type() == Node::selector_group) && (sel.type() != Node::selector_group)) {
      Node group(new_Node(Node::selector_group, sel.path(), sel.line(), pre.size()));
      for (size_t i = 0, S = pre.size(); i < S; ++i) {
        Node new_sel(new_Node(Node::selector, sel.path(), sel.line(), 2));
        if (pre[i].type() == Node::selector) new_sel += pre[i];
        else                                 new_sel << pre[i];
        if (sel.type() == Node::selector)    new_sel += sel;
        else                                 new_sel << sel;
        group << new_sel;
      }
      return group;
    }
    else if ((pre.type() != Node::selector_group) && (sel.type() == Node::selector_group)) {
      Node group(new_Node(Node::selector_group, sel.path(), sel.line(), sel.size()));
      for (size_t i = 0, S = sel.size(); i < S; ++i) {
        Node new_sel(new_Node(Node::selector, sel.path(), sel.line(), 2));
        if (pre.type() == Node::selector)    new_sel += pre;
        else                                 new_sel << pre;
        if (sel[i].type() == Node::selector) new_sel += sel[i];
        else                                 new_sel << sel[i];
        group << new_sel;
      }
      return group;
    }
    else {
      Node new_sel(new_Node(Node::selector, sel.path(), sel.line(), 2));
      if (pre.type() == Node::selector) new_sel += pre;
      else                              new_sel << pre;
      if (sel.type() == Node::selector) new_sel += sel;
      else                              new_sel << sel;
      return new_sel;
    }
    // unreachable statement
    return Node();
  }

  // Helper for expanding selectors with backrefs.
  Node expand_backref(Node sel, Node pre)
  {
    switch (sel.type())
    {
      case Node::backref: {
        return pre;
      } break;

      case Node::simple_selector_sequence:
      case Node::selector: {
        for (size_t i = 0, S = sel.size(); i < S; ++i) {
          sel[i] = expand_backref(sel[i], pre);
        }
        return sel;
      } break;

      default: {
        return sel;
      } break;
    }
    // unreachable statement
    return Node();
  }

  // Resolve selector extensions. Walk through the document tree and check each
  // selector to see whether it's the base of an extension. Needs to be a
  // separate pass after evaluation because extension requests may be located
  // within mixins, and their targets may be interpolated.
  void extend(Node expr, multimap<Node, Node>& extension_requests, Node_Factory& new_Node)
  {
    switch (expr.type())
    {
      case Node::ruleset: {
        if (!expr[2].has_been_extended()) {
          // check single selector
          if (expr[2].type() != Node::selector_group) {
            Node sel(selector_base(expr[2]));
            // if this selector has extenders ...
            size_t num_requests = extension_requests.count(sel);
            if (num_requests) {
              Node group(new_Node(Node::selector_group, sel.path(), sel.line(), 1 + num_requests));
              group << expr[2];
              // for each of its extenders ...
              for (multimap<Node, Node>::iterator request = extension_requests.lower_bound(sel);
                   request != extension_requests.upper_bound(sel);
                   ++request) {
                Node ext(generate_extension(expr[2], request->second, new_Node));
                if (ext.type() == Node::selector_group) group += ext;
                else                                    group << ext;
              }
              group = remove_duplicate_selectors(group, new_Node);
              group.has_been_extended() = true;
              expr[2] = group;
            }
          }
          // individually check each selector in a group
          else {
            Node group(expr[2]);
            Node new_group(new_Node(Node::selector_group, group.path(), group.line(), group.size()));
            // for each selector in the group ...
            for (size_t i = 0, S = group.size(); i < S; ++i) {
              new_group << group[i];
              Node sel(selector_base(group[i]));
              // if it has extenders ...
              if (!group[i].has_been_extended() && extension_requests.count(sel)) {
                // for each of its extenders ...
                for (multimap<Node, Node>::iterator request = extension_requests.lower_bound(sel);
                     request != extension_requests.upper_bound(sel);
                     ++request) {
                  Node ext(generate_extension(group[i], request->second, new_Node));
                  if (ext.type() == Node::selector_group) new_group += ext;
                  else                                    new_group << ext;
                }
                group[i].has_been_extended() = true;
              }
            }
            if (new_group.size() > 0) {
              group.has_been_extended() = true;
              new_group = remove_duplicate_selectors(new_group, new_Node);
              new_group.has_been_extended() = true;
              expr[2] = new_group;
            }
          }
        }
        extend(expr[1], extension_requests, new_Node);
      } break;

      case Node::root:
      case Node::block:
      case Node::mixin_call:
      case Node::if_directive:
      case Node::for_through_directive:
      case Node::for_to_directive:
      case Node::each_directive:
      case Node::while_directive: {
        // at this point, all directives have been expanded into style blocks,
        // so just recursively process their children
        for (size_t i = 0, S = expr.size(); i < S; ++i) {
          extend(expr[i], extension_requests, new_Node);
        }
      } break;

      default: {
        // do nothing
      } break;
    }
    return;
  }

  void extend_selectors(vector<pair<Node, Node> >& pending, multimap<Node, Node>& extension_table, Node_Factory& new_Node)
  {
    for (size_t i = 0, S = pending.size(); i < S; ++i) {
      // Node extender(pending[i].second[2]);
      // Node original_extender(pending[i].second[0]);
      Node ruleset_to_extend(pending[i].first);
      Node extendee(ruleset_to_extend[2]);

      if (extendee.type() != Node::selector_group && !extendee.has_been_extended()) {
        Node extendee_base(selector_base(extendee));
        Node extender_group(new_Node(Node::selector_group, extendee.path(), extendee.line(), 1));
        for (multimap<Node, Node>::iterator i = extension_table.lower_bound(extendee_base), E = extension_table.upper_bound(extendee_base);
             i != E;
             ++i) {
          if (i->second.size() <= 2) continue; // TODO: UN-HACKIFY THIS
          if (i->second[2].type() == Node::selector_group)
            extender_group += i->second[2];
          else
            extender_group << i->second[2];
        }
        Node extended_group(new_Node(Node::selector_group, extendee.path(), extendee.line(), extender_group.size() + 1));
        extendee.has_been_extended() = true;
        extended_group << extendee;
        for (size_t i = 0, S = extender_group.size(); i < S; ++i) {
          extended_group << generate_extension(extendee, extender_group[i], new_Node);
        }
        ruleset_to_extend[2] = extended_group;
      }
      else {
        Node extended_group(new_Node(Node::selector_group, extendee.path(), extendee.line(), extendee.size() + 1));
        for (size_t i = 0, S = extendee.size(); i < S; ++i) {
          Node extendee_i(extendee[i]);
          Node extendee_i_base(selector_base(extendee_i));
          extended_group << extendee_i;
          if (!extendee_i.has_been_extended() && extension_table.count(extendee_i_base)) {
            Node extender_group(new_Node(Node::selector_group, extendee.path(), extendee.line(), 1));
            for (multimap<Node, Node>::iterator i = extension_table.lower_bound(extendee_i_base), E = extension_table.upper_bound(extendee_i_base);
                 i != E;
                 ++i) {
              if (i->second.size() <= 2) continue; // TODO: UN-HACKIFY THIS
              if (i->second[2].type() == Node::selector_group)
                extender_group += i->second[2];
              else
                extender_group << i->second[2];
            }
            for (size_t j = 0, S = extender_group.size(); j < S; ++j) {
              extended_group << generate_extension(extendee_i, extender_group[j], new_Node);
            }
            extendee_i.has_been_extended() = true;
          }
        }
        ruleset_to_extend[2] = extended_group;
      }
    }
  }

  // Helper for generating selector extensions; called for each extendee and
  // extender in a pair of selector groups.
  Node generate_extension(Node extendee, Node extender, Node_Factory& new_Node)
  {
    Node new_group(new_Node(Node::selector_group, extendee.path(), extendee.line(), 1));
    if (extendee.type() != Node::selector) {
      switch (extender.type())
      {
        case Node::simple_selector:
        case Node::attribute_selector:
        case Node::simple_selector_sequence:
        case Node::selector: {
          new_group << extender;
          return new_group;
        } break;
        default: {
          // not sure why selectors sometimes get wrapped in a singleton group
          return generate_extension(extendee, extender[0], new_Node);
        } break;
      }
    }
    else {
      switch (extender.type())
      {
        case Node::simple_selector:
        case Node::attribute_selector:
        case Node::simple_selector_sequence: {
          Node new_ext(new_Node(Node::selector, extendee.path(), extendee.line(), extendee.size()));
          for (size_t i = 0, S = extendee.size() - 1; i < S; ++i) {
            new_ext << extendee[i];
          }
          new_ext << extender;
          new_group << new_ext;
          return new_group;
        } break;

        case Node::selector: {
          Node new_ext1(new_Node(Node::selector, extendee.path(), extendee.line(), extendee.size() + extender.size() - 1));
          Node new_ext2(new_Node(Node::selector, extendee.path(), extendee.line(), extendee.size() + extender.size() - 1));
          new_ext1 += selector_prefix(extendee, new_Node);
          new_ext1 += extender;
          new_ext2 += selector_prefix(extender, new_Node);
          new_ext2 += selector_prefix(extendee, new_Node);
          new_ext2 << extender.back();
          new_group << new_ext1 << new_ext2;
          return new_group;
        } break;

        default: {
          // something
        } break;
      }
    }
    return Node();
  }

  // Helpers for extracting subsets of selectors
  Node selector_prefix(Node sel, Node_Factory& new_Node)
  {
    switch (sel.type())
    {
      case Node::selector: {
        Node pre(new_Node(Node::selector, sel.path(), sel.line(), sel.size() - 1));
        for (size_t i = 0, S = sel.size() - 1; i < S; ++i) {
          pre << sel[i];
        }
        return pre;
      } break;

      default: {
        return new_Node(Node::selector, sel.path(), sel.line(), 0);
      } break;
    }
  }

  Node selector_base(Node sel)
  {
    switch (sel.type())
    {
      case Node::selector: {
        return sel.back();
      } break;

      default: {
        return sel;
      } break;
    }
  }

  static Node selector_but(Node sel, Node_Factory& new_Node, size_t start, size_t from_end)
  {
    switch (sel.type())
    {
      case Node::selector: {
        Node bf(new_Node(Node::selector, sel.path(), sel.line(), sel.size() - 1));
        for (size_t i = start, S = sel.size() - from_end; i < S; ++i) {
          bf << sel[i];
        }
        return bf;
      } break;

      default: {
        return new_Node(Node::selector, sel.path(), sel.line(), 0);
      } break;
    }
  }

  Node selector_butfirst(Node sel, Node_Factory& new_Node)
  { return selector_but(sel, new_Node, 1, 0); }

  Node selector_butlast(Node sel, Node_Factory& new_Node)
  { return selector_but(sel, new_Node, 0, 1); }

  void to_lowercase(string& s)
  { for (size_t i = 0, L = s.length(); i < L; ++i) s[i] = tolower(s[i]); }

  Node c_val_to_node(Sass_Value v, Context& ctx, Backtrace& bt, string path = "", size_t line = 0)
  {
    using std::strlen;
    using std::strcpy;
    Node node;
    switch (v.unknown.tag)
    {
      case SASS_BOOLEAN: {
        node = ctx.new_Node(Node::boolean, path, line, v.boolean.value);
      } break;
      case SASS_NUMBER: {
        node = ctx.new_Node(path, line, v.number.value, Node::number);
      } break;
      case SASS_PERCENTAGE: {
        node = ctx.new_Node(path, line, v.percentage.value, Node::numeric_percentage);
      } break;
      case SASS_DIMENSION: {
        char* copy = new char[strlen(v.dimension.unit)+1];
        strcpy(copy, v.dimension.unit);
        ctx.source_refs.push_back(copy);
        node = ctx.new_Node(path, line, v.dimension.value, Token::make(copy));
      } break;
      case SASS_COLOR: {
        node = ctx.new_Node(path, line, v.color.r, v.color.g, v.color.b, v.color.a);
      } break;
      case SASS_STRING: {
        char* copy = new char[strlen(v.string.value)+1];
        strcpy(copy, v.string.value);
        ctx.source_refs.push_back(copy);
        node = ctx.new_Node(Node::string_constant, path, line, Token::make(copy));
      } break;
      case SASS_LIST: {
        Node list(ctx.new_Node(Node::list, path, line, v.list.length));
        for (size_t i = 0; i < v.list.length; ++i) {
          list << c_val_to_node(v.list.values[i], ctx, bt, path, line);
        }
      } break;
      case SASS_ERROR: {
        throw_eval_error(bt, "error in C function: " + string(v.error.message), path, line);
      } break;
    }
    return node;
  }

}
