Source: genfun.js

/* -*- js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80; */
"use strict";
var Method = require("./method"),
    Role = require("./role"),
    util = require("./util");

/**
 * Creates generic functions capable of multiple dispatch across several
 * arguments. The generic function returned by this constructor is
 * functionally identical to a standard function: it can be called,
 * applied, and receives the expected binding for `this` when appropriate.
 *
 * @constructor
 * @param {object} [opts] - Options used when initializing the genfun.
 * @returns {function} New generic function.
 */
function Genfun() {
  var genfun = this;
  genfun.methods = [];
  genfun.cache = {key: [], methods: [], state: Genfun.UNINITIALIZED};
  var fun = function() {
    return apply_genfun(genfun, this, arguments);
  };
  fun.genfun = genfun;
  fun.addMethod = function(selector, func) {
    return Genfun.addMethod(genfun, selector, func);
  };
  fun.removeMethod = function(selector, func) {
    return Genfun.removeMethod(genfun, selector, func);
  };
  genfun._wrapper_function = fun;
  return fun;
}

Genfun.UNITIALIZED = 0;
Genfun.MONOMORPHIC = 1;
Genfun.POLYMORPHIC = 2;
Genfun.MEGAMORPHIC = 3;

Genfun.MAX_CACHE_SIZE = 32; // Can't inline, so the cache needs to be bigger.

/**
 * Defines a method on a generic function.
 *
 * @function
 * @param {Genfun} genfun - Genfun instance to add the method to.
 * @param {Array-like} selector - Selector array for dispatching the method.
 * @param {Function} methodFunction - Function to execute when the method
 *                                    successfully dispatches.
 */
Genfun.addMethod = function(genfun, selector, func) {
  genfun = typeof genfun === "function" &&
    genfun.genfun &&
    genfun.genfun instanceof Genfun ?
    genfun.genfun : genfun;
  if (selector.length) {
    selector = [].slice.call(selector);
    for (var i = 0; i < selector.length; i++) {
      if (!selector.hasOwnProperty(i)) {
        selector[i] = Object.prototype;
      }
    }
    var method = new Method(genfun, selector, func);
    genfun.methods.push(method);
    genfun.cache = {key: [], methods: [], state: Genfun.UNINITIALIZED};
    return method;
  } else {
    return Genfun.addMethod(
      Genfun.noApplicableMethod,
      [genfun._wrapper_function], function(_gf, newthis, args) {
        return func.apply(newthis, args);
      });
  }
};

/**
 * Removes a previously-defined method on `genfun` that matches
 * `selector` exactly.
 *
 * @function
 * @param {Genfun} genfun - Genfun to remove a method from.
 * @param {Array-like} selector - Objects to match on when finding a
 *                                    method to remove.
 */
Genfun.removeMethod = function() {
  throw new Error("not yet implemented");
};

/**
 * This generic function is called when `genfun` has been called and no
 * applicable method was found. The default method throws an `Error`.
 *
 * @function
 * @param {Genfun} genfun - Generic function instance that was called.
 * @param {*} newthis - value of `this` the genfun was called with.
 * @param {Array} callArgs - Arguments the genfun was called with.
 */
Genfun.noApplicableMethod = new Genfun();
Genfun.addMethod(Genfun.noApplicableMethod, [], function(gf, thisArg, args) {
  var msg =
        "No applicable method found when called with arguments of types: (" +
        [].map.call(args, function(arg) {
          return (/\[object ([a-zA-Z0-9]+)\]/).exec(({}).toString.call(arg))[1];
        }).join(", ") + ")",
      err = new Error(msg);
  err.genfun = gf;
  err.thisArg = thisArg;
  err.args = args;
  throw err;
});

var _current_applicable_methods,
    _current_this,
    _current_args;
function hasNextMethod() {
  if (typeof _current_applicable_methods === "undefined") {
    throw new Error("hasNextMethod and callNextMethod must "+
                    "be called inside a Genfun method.");
  } else {
    return !!_current_applicable_methods.length;
  }
}
Genfun.hasNextMethod = hasNextMethod;

function callNextMethod() {
  if (hasNextMethod()) {
    _current_args = arguments.length ? arguments : _current_args;
    _current_applicable_methods = [].slice.call(_current_applicable_methods, 1);
    return _current_applicable_methods[0].func.apply(
      _current_this, _current_args);
  } else {
    return noNextMethod();
  }
}
Genfun.callNextMethod = callNextMethod;

function noNextMethod() {
  throw new Error("No next method available");
}
Genfun.noNextMethod = noNextMethod;

/*
 * Internal
 */
function apply_genfun(genfun, newthis, args) {
  var applicable_methods = get_applicable_methods(genfun, args),
      ret, tmp_current_methods, tmp_this, tmp_args;
  if (applicable_methods.length) {
    tmp_current_methods = _current_applicable_methods;
    tmp_this = _current_this;
    tmp_args = _current_args;
    _current_applicable_methods = applicable_methods;
    _current_this = newthis;
    _current_args = args;
    ret = applicable_methods[0].func.apply(newthis, args);
    _current_applicable_methods = tmp_current_methods;
    _current_this = tmp_this;
    _current_args = tmp_args;
    return ret;
  } else {
    return Genfun.noApplicableMethod(genfun._wrapper_function, newthis, args);
  }
}

function get_applicable_methods(genfun, args) {
  var applicable_methods;
  var maybe_methods = cached_methods(genfun, args);
  if (maybe_methods) {
    applicable_methods = maybe_methods;
  } else {
    applicable_methods = compute_applicable_methods(genfun, args);
    cache_args(genfun, args, applicable_methods);
  }
  return applicable_methods;
}

function cache_args(genfun, args, methods) {
  if (genfun.cache.state === Genfun.MEGAMORPHIC) return;
  var key = [];
  var proto;
  for (var i = 0; i < args.length; i++) {
    proto = cacheable_proto(genfun, args[i]);
    if (proto) {
      key[i] = proto;
    } else {
      return;
    }
  }
  genfun.cache.key.unshift(key);
  genfun.cache.methods.unshift(methods);
  if (genfun.cache.key.length === 1) {
    genfun.cache.state = Genfun.MONOMORPHIC;
  } else if (genfun.cache.key.length < Genfun.MAX_CACHE_SIZE) {
    genfun.cache.state = Genfun.POLYMORPHIC;
  } else {
    genfun.cache.state = Genfun.MEGAMORPHIC;
  }
}

function cacheable_proto(genfun, arg) {
  var dispatchable = dispatchable_object(arg);
  if (Object.hasOwnProperty.call(dispatchable, Role.role_key_name)) {
    for (var j = 0; j < dispatchable[Role.role_key_name].length; j++) {
      var role = dispatchable[Role.role_key_name][j];
      if (role.method.genfun === genfun) {
        return undefined;
      }
    }
  }
  return Object.getPrototypeOf(dispatchable);
}

function cached_methods(genfun, args) {
  if (genfun.cache.state === Genfun.UNINITIALIZED ||
      genfun.cache.state === Genfun.MEGAMORPHIC) return;
  var protos = [];
  var proto;
  for (var i = 0; i < args.length; i++) {
    proto = cacheable_proto(genfun, args[i]);
    if (proto) {
      protos[i] = proto;
    } else {
      return;
    }
  }
  for (i = 0; i < genfun.cache.key.length; i++) {
    if (match_cached_methods(genfun.cache.key[i], protos)) {
      return genfun.cache.methods[i];
    }
  }
}

function match_cached_methods(key, protos) {
  if (key.length !== protos.length) return false;
  for (var i = 0; i < key.length; i++) {
    if (key[i] !== protos[i]) {
      return false;
    }
  }
  return true;
}

function compute_applicable_methods(genfun, args) {
  args = [].slice.call(args);
  var discovered_methods = [];
  function find_and_rank_roles(object, hierarchy_position, index) {
    var roles = Object.hasOwnProperty.call(object, Role.role_key_name) ?
          object[Role.role_key_name] :
          [];
    roles.forEach(function(role) {
      if (role.method.genfun === genfun && index === role.position) {
        if (discovered_methods.indexOf(role.method) < 0) {
          Method.clear_rank(role.method);
          discovered_methods.push(role.method);
        }
        Method.set_rank_hierarchy_position(
          role.method, index, hierarchy_position);
      }
    });
    // When a discovered method would receive more arguments than
    // were specialized, we pretend all extra arguments have a role
    // on Object.prototype.
    if (util.is_object_proto(object)) {
      discovered_methods.forEach(function(method) {
        if (method.minimal_selector <= index) {
          Method.set_rank_hierarchy_position(
            method, index, hierarchy_position);
        }
      });
    }
  }
  args.forEach(function(arg, index) {
    get_precedence_list(dispatchable_object(arg))
      .forEach(function(obj, hierarchy_position) {
        find_and_rank_roles(obj, hierarchy_position, index);
      });
  });
  var applicable_methods = discovered_methods.filter(function(method) {
    return (args.length === method._rank.length &&
            Method.is_fully_specified(method));
  });
  applicable_methods.sort(function(a, b) {
    return Method.score(a) - Method.score(b);
  });
  return applicable_methods;
}

/*
 * Helper function for getting an array representing the entire
 * inheritance/precedence chain for an object by navigating its
 * prototype pointers.
 */
function get_precedence_list(obj) {
  var precedence_list = [];
  var next_obj = obj;
  while(next_obj) {
    precedence_list.push(next_obj);
    next_obj = Object.getPrototypeOf(next_obj);
  }
  return precedence_list;
}

/*
 * Returns a useful dispatch object for value using a process similar to
 * the ToObject operation specified in http://es5.github.com/#x9.9
 */
function dispatchable_object(value) {
  // To shut up jshint, which doesn't let me turn off this warning.
  var Bool = Boolean,
      Num = Number,
      Str = String,
      Obj = Object;
  switch (typeof value) {
  case "object":
    return value;
  case "boolean":
    return new Bool(value);
  case "number":
    return new Num(value);
  case "string":
    return new Str(value);
  default:
    return new Obj(value);
  }
}

module.exports = Genfun;