• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const Method = require('./method')
4const Role = require('./role')
5const util = require('./util')
6
7const kCache = Symbol('cache')
8const kDefaultMethod = Symbol('defaultMethod')
9const kMethods = Symbol('methods')
10const kNoNext = Symbol('noNext')
11
12module.exports = function genfun (opts) {
13  function gf () {
14    if (!gf[kMethods].length && gf[kDefaultMethod]) {
15      return gf[kDefaultMethod].func.apply(this, arguments)
16    } else {
17      return gf.applyGenfun(this, arguments)
18    }
19  }
20  Object.setPrototypeOf(gf, Genfun.prototype)
21  gf[kMethods] = []
22  gf[kCache] = {key: [], methods: [], state: STATES.UNINITIALIZED}
23  if (opts && typeof opts === 'function') {
24    gf.add(opts)
25  } else if (opts && opts.default) {
26    gf.add(opts.default)
27  }
28  if (opts && opts.name) {
29    Object.defineProperty(gf, 'name', {
30      value: opts.name
31    })
32  }
33  if (opts && opts.noNextMethod) {
34    gf[kNoNext] = true
35  }
36  return gf
37}
38
39class Genfun extends Function {}
40Genfun.prototype.isGenfun = true
41
42const STATES = {
43  UNINITIALIZED: 0,
44  MONOMORPHIC: 1,
45  POLYMORPHIC: 2,
46  MEGAMORPHIC: 3
47}
48
49const MAX_CACHE_SIZE = 32
50
51/**
52 * Defines a method on a generic function.
53 *
54 * @function
55 * @param {Array-like} selector - Selector array for dispatching the method.
56 * @param {Function} methodFunction - Function to execute when the method
57 *                                    successfully dispatches.
58 */
59Genfun.prototype.add = function addMethod (selector, func) {
60  if (!func && typeof selector === 'function') {
61    func = selector
62    selector = []
63  }
64  selector = [].slice.call(selector)
65  for (var i = 0; i < selector.length; i++) {
66    if (!selector.hasOwnProperty(i)) {
67      selector[i] = Object.prototype
68    }
69  }
70  this[kCache] = {key: [], methods: [], state: STATES.UNINITIALIZED}
71  let method = new Method(this, selector, func)
72  if (selector.length) {
73    this[kMethods].push(method)
74  } else {
75    this[kDefaultMethod] = method
76  }
77  return this
78}
79
80/**
81 * Removes a previously-defined method on `genfun` that matches
82 * `selector` exactly.
83 *
84 * @function
85 * @param {Genfun} genfun - Genfun to remove a method from.
86 * @param {Array-like} selector - Objects to match on when finding a
87 *                                    method to remove.
88 */
89Genfun.prototype.rm = function removeMethod () {
90  throw new Error('not yet implemented')
91}
92
93/**
94 * Returns true if there are methods that apply to the given arguments on
95 * `genfun`. Additionally, makes sure the cache is warmed up for the given
96 * arguments.
97 *
98 */
99Genfun.prototype.hasMethod = function hasMethod () {
100  const methods = this.getApplicableMethods(arguments)
101  return !!(methods && methods.length)
102}
103
104/**
105 * This generic function is called when `genfun` has been called and no
106 * applicable method was found. The default method throws an `Error`.
107 *
108 * @function
109 * @param {Genfun} genfun - Generic function instance that was called.
110 * @param {*} newthis - value of `this` the genfun was called with.
111 * @param {Array} callArgs - Arguments the genfun was called with.
112 */
113module.exports.noApplicableMethod = module.exports()
114module.exports.noApplicableMethod.add([], (gf, thisArg, args) => {
115  let msg =
116        'No applicable method found when called with arguments of types: (' +
117        [].map.call(args, (arg) => {
118          return (/\[object ([a-zA-Z0-9]+)\]/)
119            .exec(({}).toString.call(arg))[1]
120        }).join(', ') + ')'
121  let err = new Error(msg)
122  err.genfun = gf
123  err.thisArg = thisArg
124  err.args = args
125  throw err
126})
127
128/*
129 * Internal
130 */
131Genfun.prototype.applyGenfun = function applyGenfun (newThis, args) {
132  let applicableMethods = this.getApplicableMethods(args)
133  if (applicableMethods.length === 1 || this[kNoNext]) {
134    return applicableMethods[0].func.apply(newThis, args)
135  } else if (applicableMethods.length > 1) {
136    let idx = 0
137    const nextMethod = function nextMethod () {
138      if (arguments.length) {
139        // Replace args if passed in explicitly
140        args = arguments
141        Array.prototype.push.call(args, nextMethod)
142      }
143      const next = applicableMethods[idx++]
144      if (idx >= applicableMethods.length) {
145        Array.prototype.pop.call(args)
146      }
147      return next.func.apply(newThis, args)
148    }
149    Array.prototype.push.call(args, nextMethod)
150    return nextMethod()
151  } else {
152    return module.exports.noApplicableMethod(this, newThis, args)
153  }
154}
155
156Genfun.prototype.getApplicableMethods = function getApplicableMethods (args) {
157  if (!args.length || !this[kMethods].length) {
158    return this[kDefaultMethod] ? [this[kDefaultMethod]] : []
159  }
160  let applicableMethods
161  let maybeMethods = cachedMethods(this, args)
162  if (maybeMethods) {
163    applicableMethods = maybeMethods
164  } else {
165    applicableMethods = computeApplicableMethods(this, args)
166    cacheArgs(this, args, applicableMethods)
167  }
168  return applicableMethods
169}
170
171function cacheArgs (genfun, args, methods) {
172  if (genfun[kCache].state === STATES.MEGAMORPHIC) { return }
173  var key = []
174  var proto
175  for (var i = 0; i < args.length; i++) {
176    proto = cacheableProto(genfun, args[i])
177    if (proto) {
178      key[i] = proto
179    } else {
180      return null
181    }
182  }
183  genfun[kCache].key.unshift(key)
184  genfun[kCache].methods.unshift(methods)
185  if (genfun[kCache].key.length === 1) {
186    genfun[kCache].state = STATES.MONOMORPHIC
187  } else if (genfun[kCache].key.length < MAX_CACHE_SIZE) {
188    genfun[kCache].state = STATES.POLYMORPHIC
189  } else {
190    genfun[kCache].state = STATES.MEGAMORPHIC
191  }
192}
193
194function cacheableProto (genfun, arg) {
195  var dispatchable = util.dispatchableObject(arg)
196  if (Object.hasOwnProperty.call(dispatchable, Role.roleKeyName)) {
197    for (var j = 0; j < dispatchable[Role.roleKeyName].length; j++) {
198      var role = dispatchable[Role.roleKeyName][j]
199      if (role.method.genfun === genfun) {
200        return null
201      }
202    }
203  }
204  return Object.getPrototypeOf(dispatchable)
205}
206
207function cachedMethods (genfun, args) {
208  if (genfun[kCache].state === STATES.UNINITIALIZED ||
209      genfun[kCache].state === STATES.MEGAMORPHIC) {
210    return null
211  }
212  var protos = []
213  var proto
214  for (var i = 0; i < args.length; i++) {
215    proto = cacheableProto(genfun, args[i])
216    if (proto) {
217      protos[i] = proto
218    } else {
219      return
220    }
221  }
222  for (i = 0; i < genfun[kCache].key.length; i++) {
223    if (matchCachedMethods(genfun[kCache].key[i], protos)) {
224      return genfun[kCache].methods[i]
225    }
226  }
227}
228
229function matchCachedMethods (key, protos) {
230  if (key.length !== protos.length) { return false }
231  for (var i = 0; i < key.length; i++) {
232    if (key[i] !== protos[i]) {
233      return false
234    }
235  }
236  return true
237}
238
239function computeApplicableMethods (genfun, args) {
240  args = [].slice.call(args)
241  let discoveredMethods = []
242  function findAndRankRoles (object, hierarchyPosition, index) {
243    var roles = Object.hasOwnProperty.call(object, Role.roleKeyName)
244    ? object[Role.roleKeyName]
245    : []
246    roles.forEach(role => {
247      if (role.method.genfun === genfun && index === role.position) {
248        if (discoveredMethods.indexOf(role.method) < 0) {
249          Method.clearRank(role.method)
250          discoveredMethods.push(role.method)
251        }
252        Method.setRankHierarchyPosition(role.method, index, hierarchyPosition)
253      }
254    })
255    // When a discovered method would receive more arguments than
256    // were specialized, we pretend all extra arguments have a role
257    // on Object.prototype.
258    if (util.isObjectProto(object)) {
259      discoveredMethods.forEach(method => {
260        if (method.minimalSelector <= index) {
261          Method.setRankHierarchyPosition(method, index, hierarchyPosition)
262        }
263      })
264    }
265  }
266  args.forEach((arg, index) => {
267    getPrecedenceList(util.dispatchableObject(arg))
268      .forEach((obj, hierarchyPosition) => {
269        findAndRankRoles(obj, hierarchyPosition, index)
270      })
271  })
272  let applicableMethods = discoveredMethods.filter(method => {
273    return (args.length === method._rank.length &&
274            Method.isFullySpecified(method))
275  })
276  applicableMethods.sort((a, b) => Method.score(a) - Method.score(b))
277  if (genfun[kDefaultMethod]) {
278    applicableMethods.push(genfun[kDefaultMethod])
279  }
280  return applicableMethods
281}
282
283/*
284 * Helper function for getting an array representing the entire
285 * inheritance/precedence chain for an object by navigating its
286 * prototype pointers.
287 */
288function getPrecedenceList (obj) {
289  var precedenceList = []
290  var nextObj = obj
291  while (nextObj) {
292    precedenceList.push(nextObj)
293    nextObj = Object.getPrototypeOf(nextObj)
294  }
295  return precedenceList
296}
297