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