genfun.js 8.4 KB

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