├── .babelrc ├── .gitignore ├── README.md ├── lib ├── data.js ├── decorator.js ├── define-operator.js └── index.js ├── package.json ├── src ├── data.js ├── decorator.js ├── define-operator.js └── index.js └── test ├── complex.js ├── test-spec.js └── test-util.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-1"], 3 | "plugins": ["transform-decorators-legacy"] 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Operator Overloading 2 | 3 | In some situations operator overloading can result in code that's easier to 4 | write and easier to read. 5 | 6 | __without overloading__ 7 | ```javascript 8 | let u = new Vector(1, 0); 9 | let v = new Vector(2, -1); 10 | 11 | let w = u.add(v); 12 | w = w.plus(v.scale(3)); 13 | ``` 14 | 15 | __with overloading__ 16 | ```javascript 17 | let u = new Vector(1, 0); 18 | let v = new Vector(2, -1); 19 | 20 | let w = u + v; 21 | w += 3 * v; 22 | ``` 23 | 24 | ## `Function.defineOperator` 25 | 26 | Binary operators are defined as follows: 27 | ```javascript 28 | Function.defineOperator( 29 | '+', 30 | [Vector, Vector], 31 | (u, v) => new Vector(u.x + v.x, u.y + v.y) 32 | ); 33 | ``` 34 | 35 | Unary operators are defined as follows: 36 | ```javascript 37 | Function.defineOperator( 38 | '-', 39 | [Vector], 40 | (v) => new Vector(-v.x, -v.y) 41 | ); 42 | ``` 43 | 44 | __Notes:__ 45 | - Commutative operators, `+`, `*`, `&&`, `||`, `&`, `|`, `^`, automatically 46 | flip the order of operands when their types are different. 47 | - `Function.defineOperator(T == T, (a, b) => fn(a, b)` will automatically define 48 | `!=` as `(a, b) => !fn(a, b)`. 49 | - `!` and `!=` cannot be overloaded in order to perserve identities: 50 | 51 | ``` 52 | X ? A : B <=> !X ? B : A 53 | !(X && Y) <=> !X || !Y 54 | !(X || Y) <=> !X && !Y 55 | X != Y <=> !(X == Y) 56 | ``` 57 | Source: http://www.slideshare.net/BrendanEich/js-resp (page 7) 58 | - `>` and `>=` are derived from `<` and `<=` as follows: 59 | 60 | ``` 61 | A > B <=> B < A 62 | A >= B <=> B <= A 63 | ``` 64 | Source: http://www.slideshare.net/BrendanEich/js-resp (page 8) 65 | - Redefining some operators on some built-in types is prohibited. The reason 66 | being that operator overloading should be used to make classes that don't 67 | have operator support easier to work with and prevent changing behavior of 68 | those classes do that. 69 | - all operators on `[Number, Number]` 70 | - logical operators on `[Boolean, Boolean]` 71 | - `+` on `[String, String]` 72 | - unary `+` and `-` on [Number] 73 | 74 | ## `'use overloading'` directive 75 | 76 | The `'use overloading'` directive can be used to limit the scope of overloading 77 | can be used. This directive is opt-in because for existing code it will have 78 | negative performance impacts. In general, overloading should be used where 79 | readability is more important that performance. 80 | 81 | It can be used at the start of a file or function/method definition. The 82 | `@operator` section has an example of the `'use overloading'` directive in action. 83 | 84 | ## `@operator` decorator 85 | 86 | The `@operator` decorator is a convenience for declaring methods as operators 87 | when defining a class. 88 | 89 | ```javascript 90 | class Vector { 91 | constructor(x, y) { 92 | Object.assign(this, { x, y }); 93 | } 94 | 95 | @operator('+') 96 | add(other) { 97 | return new Vector(this.x + other.x, this.y + other.y); 98 | } 99 | 100 | @operator('-') 101 | neg() { 102 | return new Vector(-this.x, -this.y); 103 | } 104 | 105 | @operator('-') 106 | sub(other) { 107 | 'use overloading'; 108 | return this + -other; 109 | } 110 | 111 | @operator('*', Number) 112 | scale(factor) { 113 | return new Vector(factor * this.x, factor * this.y); 114 | } 115 | } 116 | ``` 117 | 118 | The `@operator` decorator makes the assumption that both operands are the same 119 | type as the class. If this is not the case, the type of the other operand can 120 | be specified as the second parameter to `@operator`. 121 | 122 | ## Implementation Details 123 | 124 | The following code: 125 | 126 | ```javascript 127 | 'use overloading' 128 | 129 | let u = new Vector(1, 0); 130 | let v = new Vector(2, -1); 131 | 132 | let w = u + v; 133 | w += 3 * v; 134 | ``` 135 | 136 | relies one the following operators to be defined: 137 | 138 | ```javascript 139 | Function.defineOperator(Vector + Vector, 140 | (u, v) => new Vector(u.x + v.x, u.y + v.y); 141 | 142 | Function.defineOperator(Number * Vector, (k, v)) 143 | ``` 144 | 145 | and compiles to: 146 | 147 | ```javascript 148 | let u = new Vector(1, 0); 149 | let v = new Vector(2, -1); 150 | 151 | let w = Function[Symbol.plus](u, v); 152 | w = Function[Symbol.plus](w, Function[Symbol.times](3, v)); 153 | ``` 154 | 155 | The implementation defines the following well-known Symbols: 156 | 157 | __Binary Operators__ 158 | - Symbol.plus `+` 159 | - Symbol.minus `-` 160 | - Symbol.times `*` 161 | - Symbol.divide `/` 162 | - Symbol.remainder `%` 163 | - Symbol.equality `==` 164 | - Symbol.inequality `!=` 165 | - Symbol.lessThan `<` 166 | - Symbol.lessThanOrEqual `<=` 167 | - Symbol.greaterThan `>` 168 | - Symbol.greaterThanOrEqual `>=` 169 | - Symbol.shiftLeft `<<` 170 | - Symbol.shiftRight `>>` 171 | - Symbol.unsignedShiftRight `>>>` 172 | - Symbol.bitwiseOr `|` 173 | - Symbol.bitwiseAnd `&` 174 | - Symbol.bitwiseXor `^` 175 | - Symbol.logicalOr `||` 176 | - Symbol.logicalAnd `&&` 177 | 178 | __Unary Operators__ 179 | - Symbol.unaryPlus `+` 180 | - Symbol.unaryMinus `-` 181 | - Symbol.bitwiseNot `~` 182 | 183 | __Note:__ only the following operators can actually be overloaded: 184 | `|`, `^`, `&`, `==`, `<`, `<=`, `<<`, `>>`, `>>>`, `+`, `-`, `*`, `/`, `%`, 185 | `~`, unary`-`, and unary`+` 186 | 187 | ### Function Lookup 188 | 189 | The functions for each operator are stored in a lookup table. When a call to 190 | `Function.defineOperator` is made, we get the `prototype` for the types of the 191 | arguments. The prototypes are stored in a protoypes array the index of the 192 | `prototype` from that array is used to determine the key in the lookup table. 193 | 194 | In the case of unary operators the index is the key. For binary operators, the 195 | index is a string with the two indices separate by commas. 196 | 197 | TODO: describe how prototype chain support works. 198 | 199 | ## Future Work 200 | 201 | - [x] handle prototype chain 202 | - [ ] support exponentiation operator 203 | - [ ] use static type information to improve performance (could determine which 204 | function to call at compile time) 205 | 206 | -------------------------------------------------------------------------------- /lib/data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.binaryOperators = { 4 | '==': 'equality', 5 | '!=': 'inequality', 6 | 7 | '<': 'lessThan', 8 | '<=': 'lessThanOrEqual', 9 | '>': 'greaterThan', 10 | '>=': 'greaterThanOrEqual', 11 | 12 | '<<': 'shiftLeft', 13 | '>>': 'shiftRight', 14 | '>>>': 'unsignedShiftRight', 15 | 16 | '+': 'plus', 17 | '-': 'minus', 18 | '*': 'times', 19 | '/': 'divide', 20 | '%': 'remainder', 21 | '**': 'power', 22 | 23 | '|': 'bitwiseOr', 24 | '^': 'bitwiseXor', 25 | '&': 'bitwiseAnd' 26 | }; 27 | 28 | exports.logicalOperators = { 29 | '||': 'logicalOr', 30 | '&&': 'logicalAnd' 31 | }; 32 | 33 | exports.unaryOperators = { 34 | '+': 'unaryPlus', 35 | '-': 'unaryMinus', 36 | 37 | '!': 'logicalNot', 38 | '~': 'bitwiseNot' 39 | }; -------------------------------------------------------------------------------- /lib/decorator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var data = require('./data'); 4 | 5 | // TODO: handle static methods by checking typeof target === "function" 6 | module.exports = function operator(op, otherType) { 7 | return function (target, name, desc) { 8 | var ctor = target.constructor; 9 | switch (desc.value.length) { 10 | case 0: 11 | if (op in data.unaryOperators) { 12 | Function.defineOperator(op, [ctor], function (a) { 13 | return desc.value.call(a); 14 | }); 15 | } else { 16 | throw new Error(op + ' not a valid unary operator'); 17 | } 18 | break; 19 | case 1: 20 | if (op in data.binaryOperators || op in data.logicalOperators) { 21 | Function.defineOperator(op, [ctor, otherType || ctor], function (a, b) { 22 | return desc.value.call(a, b); 23 | }); 24 | } else { 25 | throw new Error(op + ' not a binary valid operator'); 26 | } 27 | break; 28 | default: 29 | throw new Error('@operator accepts at most one argument'); 30 | break; 31 | } 32 | }; 33 | }; -------------------------------------------------------------------------------- /lib/define-operator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 4 | 5 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 6 | 7 | var assert = require('assert'); 8 | 9 | var prototypes = [Object.prototype, String.prototype, Number.prototype]; 10 | 11 | // key is the index of prototye from prototypes 12 | var prototypeChains = { 13 | '0': ['0'], 14 | '1': ['1', '0'], 15 | '2': ['2', '0'] 16 | }; 17 | 18 | var operators = { 19 | '+': {}, 20 | '-': {}, 21 | '*': {}, 22 | '/': {}, 23 | '%': {}, 24 | '==': {}, 25 | '!=': {}, 26 | '<': {}, 27 | '<=': {}, 28 | '>': {}, 29 | '>=': {}, 30 | '<<': {}, 31 | '>>': {}, 32 | '>>>': {}, 33 | '|': {}, 34 | '&': {}, 35 | '^': {}, 36 | '~': {}, 37 | '||': {}, 38 | '&&': {} 39 | }; 40 | 41 | var commutatives = ['+', '*', '&&', '||', '&', '|', '^', '==', '!=']; 42 | 43 | // TODO: check if we already have a prototype chain for this prototype 44 | var computePrototypeChain = function computePrototypeChain(proto) { 45 | var chain = []; 46 | 47 | while (proto !== null) { 48 | if (!prototypes.includes(proto)) { 49 | prototypes.push(proto); 50 | } 51 | var index = prototypes.indexOf(proto); 52 | chain.push(index); 53 | 54 | proto = Object.getPrototypeOf(proto); 55 | } 56 | 57 | while (chain.length > 0) { 58 | prototypeChains[chain[0]] = chain; 59 | chain = chain.slice(1); 60 | } 61 | }; 62 | 63 | var defineBinaryOperator = function defineBinaryOperator(op, types, fn) { 64 | var _types = _slicedToArray(types, 2); 65 | 66 | var a = _types[0]; 67 | var b = _types[1]; 68 | 69 | 70 | if (typeof a !== 'function' || typeof b !== 'function') { 71 | throw new Error('Both types must be functions/classes'); 72 | } 73 | 74 | var aProto = a.prototype; 75 | var bProto = b.prototype; 76 | 77 | if (aProto === Number.prototype && bProto === Number.prototype) { 78 | throw new Error('redefining \'' + op + '\' for [Number, Number] is prohibited'); 79 | } 80 | 81 | if (aProto === Boolean.prototype && bProto === Boolean.prototype) { 82 | if (['||', '&&'].includes(op)) { 83 | throw new Error('redefining \'' + op + '\' for [Boolean, Boolean] is prohibited'); 84 | } 85 | } 86 | 87 | if (op === '+' && aProto === String.prototype && bProto === String.prototype) { 88 | throw new Error('redefining \'+\' for [String, String] is prohibited'); 89 | } 90 | 91 | if (!prototypes.includes(aProto)) { 92 | prototypes.push(aProto); 93 | } 94 | 95 | if (!prototypes.includes(bProto)) { 96 | prototypes.push(bProto); 97 | } 98 | 99 | var aid = prototypes.indexOf(aProto); 100 | if (!prototypeChains.hasOwnProperty(aid)) { 101 | computePrototypeChain(aProto); 102 | } 103 | 104 | var bid = prototypes.indexOf(bProto); 105 | if (!prototypeChains.hasOwnProperty(bid)) { 106 | computePrototypeChain(bProto); 107 | } 108 | 109 | var id = aid + ',' + bid; 110 | 111 | operators[op][id] = fn; 112 | 113 | // handle commutative operations automatically 114 | if (commutatives.includes(op) && a !== b) { 115 | // reverse the arguments so that we can deal with any special cases 116 | // involving types that aren't the same 117 | operators[op][bid + ',' + aid] = function (a, b) { 118 | return fn(b, a); 119 | }; 120 | } else if (op === '<') { 121 | operators['>'][bid + ',' + aid] = function (a, b) { 122 | return fn(b, a); 123 | }; 124 | } else if (op === '<=') { 125 | operators['>='][bid + ',' + aid] = function (a, b) { 126 | return fn(b, a); 127 | }; 128 | } else if (op === '==') { 129 | operators['!='][aid + ',' + bid] = function (a, b) { 130 | return !fn(a, b); 131 | }; 132 | operators['!='][bid + ',' + aid] = function (a, b) { 133 | return !fn(b, a); 134 | }; 135 | } 136 | }; 137 | 138 | var defineUnaryOperator = function defineUnaryOperator(op, types, fn) { 139 | var _types2 = _slicedToArray(types, 1); 140 | 141 | var a = _types2[0]; 142 | 143 | 144 | if (typeof a !== 'function') { 145 | throw new Error('Type must be a function/class'); 146 | } 147 | 148 | var aProto = a.prototype; 149 | 150 | if (aProto === Number.prototype) { 151 | throw new Error('redefining \'' + op + '\' for [Number] is prohibited'); 152 | } 153 | 154 | if (!prototypes.includes(aProto)) { 155 | prototypes.push(aProto); 156 | } 157 | 158 | var id = prototypes.indexOf(aProto); 159 | 160 | if (!prototypeChains.hasOwnProperty(id)) { 161 | computePrototypeChain(aProto); 162 | } 163 | 164 | operators[op][id] = fn; 165 | }; 166 | 167 | var allowedOperators = ['|', '^', '&', '~', '==', '<', '<=', '<<', '>>', '>>>', '+', '-', '*', '/', '%']; 168 | 169 | Function.defineOperator = function (op, types, fn) { 170 | if (!allowedOperators.includes(op)) { 171 | throw new Error('\'' + op + '\' cannot be overloaded'); 172 | } 173 | 174 | if (types.length === 2) { 175 | assert(fn.length === 2, 'function takes ' + fn.length + ' params but should take 2'); 176 | return defineBinaryOperator(op, types, fn); 177 | } else if (types.length === 1) { 178 | assert(fn.length === 1, 'function takes ' + fn.length + ' params but should take 1'); 179 | return defineUnaryOperator(op, types, fn); 180 | } 181 | }; 182 | 183 | var operatorData = { 184 | plus: ['+', function (a, b) { 185 | return a + b; 186 | }], 187 | minus: ['-', function (a, b) { 188 | return a - b; 189 | }], 190 | times: ['*', function (a, b) { 191 | return a * b; 192 | }], 193 | divide: ['/', function (a, b) { 194 | return a / b; 195 | }], 196 | remainder: ['%', function (a, b) { 197 | return a / b; 198 | }], 199 | unaryPlus: ['+', function (a) { 200 | return +a; 201 | }], 202 | unaryMinus: ['-', function (a) { 203 | return -a; 204 | }], 205 | 206 | equality: ['==', function (a, b) { 207 | return a == b; 208 | }], 209 | inequality: ['!=', function (a, b) { 210 | return a != b; 211 | }], 212 | lessThan: ['<', function (a, b) { 213 | return a !== b; 214 | }], 215 | lessThanOrEqual: ['<=', function (a, b) { 216 | return a !== b; 217 | }], 218 | greaterThan: ['>', function (a, b) { 219 | return a !== b; 220 | }], 221 | greaterThanOrEqual: ['>=', function (a, b) { 222 | return a !== b; 223 | }], 224 | 225 | shiftLeft: ['<<', function (a, b) { 226 | return a << b; 227 | }], 228 | shiftRight: ['>>', function (a, b) { 229 | return a >> b; 230 | }], 231 | unsignedShiftRight: ['>>>', function (a, b) { 232 | return a >>> b; 233 | }], 234 | bitwiseOr: ['|', function (a, b) { 235 | return a | b; 236 | }], 237 | bitwiseAnd: ['&', function (a, b) { 238 | return a & b; 239 | }], 240 | bitwiseXor: ['^', function (a, b) { 241 | return a ^ b; 242 | }], 243 | bitwiseNot: ['~', function (a) { 244 | return ~a; 245 | }], 246 | 247 | logicalOr: ['||', function (a, b) { 248 | return a || b; 249 | }], 250 | logicalAnd: ['&&', function (a, b) { 251 | return a && b; 252 | }] 253 | }; 254 | 255 | Object.keys(operatorData).forEach(function (name) { 256 | var op = operatorData[name][0]; 257 | var fn = operatorData[name][1]; 258 | 259 | var id = fn.length === 2 ? '0,0' : '0'; 260 | operators[op][id] = fn; 261 | 262 | var sym = Symbol[name] = Symbol(name); 263 | var objProto = Object.prototype; 264 | 265 | if (fn.length === 2) { 266 | Function[sym] = function (a, b) { 267 | var aProto = void 0, 268 | bProto = void 0; 269 | 270 | if (a != null) { 271 | aProto = Object.getPrototypeOf(a); 272 | if (aProto !== objProto && !prototypes.includes(aProto)) { 273 | computePrototypeChain(aProto); 274 | } 275 | } 276 | 277 | if (b != null) { 278 | bProto = Object.getPrototypeOf(b); 279 | if (bProto !== objProto && !prototypes.includes(bProto)) { 280 | computePrototypeChain(bProto); 281 | } 282 | } 283 | 284 | var aid = a == null ? 0 : prototypes.indexOf(aProto); 285 | var bid = b == null ? 0 : prototypes.indexOf(bProto); 286 | 287 | // optimize for an exact match of the operand prototypes 288 | var fastId = aid + ',' + bid; 289 | if (operators[op][fastId]) { 290 | var _fn = operators[op][fastId]; 291 | return _fn(a, b); 292 | } 293 | 294 | // We copy the prototype chains so that we don't modify them. 295 | var chainA = [].concat(_toConsumableArray(prototypeChains[aid])); 296 | var chainB = [].concat(_toConsumableArray(prototypeChains[bid])); 297 | 298 | var ids = []; 299 | 300 | // TODO: if the operator is commutative we can simplify this a bit 301 | while (chainA.length > 1 && chainB.length > 1) { 302 | if (chainA.length > chainB.length) { 303 | ids.push.apply(ids, _toConsumableArray(chainA.map(function (id) { 304 | return id + ',' + chainB[0]; 305 | }))); 306 | chainA.shift(); 307 | } else if (chainB.length > chainA.length) { 308 | ids.push.apply(ids, _toConsumableArray(chainB.map(function (id) { 309 | return chainA[0] + ',' + id; 310 | }))); 311 | chainB.shift(); 312 | } else { 313 | ids.push(chainA[0] + ',' + chainB[0]); 314 | // Ensure the the sum of the chain lengths of each pair of 315 | // prototype chains is monotonically decrease. 316 | for (var i = 1; i < chainA.length; i++) { 317 | ids.push(chainA[0] + ',' + chainB[i]); 318 | ids.push(chainA[i] + ',' + chainB[0]); 319 | } 320 | chainA.shift(); 321 | chainB.shift(); 322 | } 323 | } 324 | 325 | // base case 326 | ids.push('0,0'); 327 | 328 | var id = ids.find(function (id) { 329 | return operators[op][id]; 330 | }); 331 | 332 | var fn = operators[op][id]; 333 | return fn(a, b); 334 | }; 335 | } else { 336 | Function[sym] = function (a) { 337 | if (a != null) { 338 | var aProto = Object.getPrototypeOf(a); 339 | if (aProto !== objProto && !prototypes.includes(aProto)) { 340 | computePrototypeChain(aProto); 341 | } 342 | } 343 | 344 | var aid = prototypes.indexOf(Object.getPrototypeOf(a)); 345 | var id = prototypeChains[aid].find(function (id) { 346 | return operators[op][id]; 347 | }); 348 | 349 | var fn = operators[op][id]; 350 | return fn(a); 351 | }; 352 | } 353 | }); -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./define-operator'); 4 | 5 | module.exports = { 6 | operator: require('./decorator') 7 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "define-operator", 3 | "version": "0.0.5", 4 | "main": "lib/index.js", 5 | "files": ["lib/*.js"], 6 | "scripts": { 7 | "test": "mocha ./test/test-spec.js --compilers js:babel-register", 8 | "prepublish": "babel src --out-dir lib" 9 | }, 10 | "author": "Kevin Barabash ", 11 | "license": "MIT", 12 | "description": "", 13 | "dependencies": { 14 | "escodegen": "^1.8.0", 15 | "esprima": "^2.7.2", 16 | "estraverse": "^4.2.0" 17 | }, 18 | "devDependencies": { 19 | "babel-cli": "^6.8.0", 20 | "babel-core": "^6.7.7", 21 | "babel-plugin-operator-overloading": "0.0.5", 22 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 23 | "babel-polyfill": "^6.7.4", 24 | "babel-preset-es2015": "^6.6.0", 25 | "babel-preset-stage-1": "^6.5.0", 26 | "babel-register": "^6.7.2", 27 | "mocha": "^2.4.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/data.js: -------------------------------------------------------------------------------- 1 | exports.binaryOperators = { 2 | '==': 'equality', 3 | '!=': 'inequality', 4 | 5 | '<': 'lessThan', 6 | '<=': 'lessThanOrEqual', 7 | '>': 'greaterThan', 8 | '>=': 'greaterThanOrEqual', 9 | 10 | '<<': 'shiftLeft', 11 | '>>': 'shiftRight', 12 | '>>>': 'unsignedShiftRight', 13 | 14 | '+': 'plus', 15 | '-': 'minus', 16 | '*': 'times', 17 | '/': 'divide', 18 | '%': 'remainder', 19 | '**': 'power', 20 | 21 | '|': 'bitwiseOr', 22 | '^': 'bitwiseXor', 23 | '&': 'bitwiseAnd', 24 | }; 25 | 26 | exports.logicalOperators = { 27 | '||': 'logicalOr', 28 | '&&': 'logicalAnd', 29 | }; 30 | 31 | exports.unaryOperators = { 32 | '+': 'unaryPlus', 33 | '-': 'unaryMinus', 34 | 35 | '!': 'logicalNot', 36 | '~': 'bitwiseNot', 37 | }; 38 | -------------------------------------------------------------------------------- /src/decorator.js: -------------------------------------------------------------------------------- 1 | const data = require('./data'); 2 | 3 | // TODO: handle static methods by checking typeof target === "function" 4 | module.exports = function operator(op, otherType) { 5 | return function(target, name, desc) { 6 | const ctor = target.constructor; 7 | switch (desc.value.length) { 8 | case 0: 9 | if (op in data.unaryOperators) { 10 | Function.defineOperator( 11 | op, 12 | [ctor], 13 | (a) => desc.value.call(a) 14 | ); 15 | } else { 16 | throw new Error(`${op} not a valid unary operator`); 17 | } 18 | break; 19 | case 1: 20 | if (op in data.binaryOperators || op in data.logicalOperators) { 21 | Function.defineOperator( 22 | op, 23 | [ctor, otherType || ctor], 24 | (a, b) => desc.value.call(a, b) 25 | ); 26 | } else { 27 | throw new Error(`${op} not a binary valid operator`); 28 | } 29 | break; 30 | default: 31 | throw new Error(`@operator accepts at most one argument`); 32 | break; 33 | } 34 | 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/define-operator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const prototypes = [ 6 | Object.prototype, String.prototype, Number.prototype, 7 | ]; 8 | 9 | // key is the index of prototye from prototypes 10 | const prototypeChains = { 11 | '0': ['0'], 12 | '1': ['1', '0'], 13 | '2': ['2', '0'], 14 | }; 15 | 16 | const operators = { 17 | '+': {}, 18 | '-': {}, 19 | '*': {}, 20 | '/': {}, 21 | '%': {}, 22 | '==': {}, 23 | '!=': {}, 24 | '<': {}, 25 | '<=': {}, 26 | '>': {}, 27 | '>=': {}, 28 | '<<': {}, 29 | '>>': {}, 30 | '>>>': {}, 31 | '|': {}, 32 | '&': {}, 33 | '^': {}, 34 | '~': {}, 35 | '||': {}, 36 | '&&': {}, 37 | }; 38 | 39 | const commutatives = [ 40 | '+', '*', '&&', '||', '&', '|', '^', '==', '!=' 41 | ]; 42 | 43 | // TODO: check if we already have a prototype chain for this prototype 44 | const computePrototypeChain = function(proto) { 45 | let chain = []; 46 | 47 | while (proto !== null) { 48 | if (!prototypes.includes(proto)) { 49 | prototypes.push(proto); 50 | } 51 | const index = prototypes.indexOf(proto); 52 | chain.push(index); 53 | 54 | proto = Object.getPrototypeOf(proto); 55 | } 56 | 57 | while (chain.length > 0) { 58 | prototypeChains[chain[0]] = chain; 59 | chain = chain.slice(1); 60 | } 61 | }; 62 | 63 | const defineBinaryOperator = function(op, types, fn) { 64 | const [a, b] = types; 65 | 66 | if (typeof a !== 'function' || typeof b !== 'function') { 67 | throw new Error('Both types must be functions/classes'); 68 | } 69 | 70 | const aProto = a.prototype; 71 | const bProto = b.prototype; 72 | 73 | if (aProto === Number.prototype && bProto === Number.prototype) { 74 | throw new Error(`redefining '${op}' for [Number, Number] is prohibited`); 75 | } 76 | 77 | if (aProto === Boolean.prototype && bProto === Boolean.prototype) { 78 | if (['||', '&&'].includes(op)) { 79 | throw new Error(`redefining '${op}' for [Boolean, Boolean] is prohibited`); 80 | } 81 | } 82 | 83 | if (op === '+' && aProto === String.prototype && bProto === String.prototype) { 84 | throw new Error(`redefining '+' for [String, String] is prohibited`); 85 | } 86 | 87 | if (!prototypes.includes(aProto)) { 88 | prototypes.push(aProto); 89 | } 90 | 91 | if (!prototypes.includes(bProto)) { 92 | prototypes.push(bProto); 93 | } 94 | 95 | const aid = prototypes.indexOf(aProto); 96 | if (!prototypeChains.hasOwnProperty(aid)) { 97 | computePrototypeChain(aProto); 98 | } 99 | 100 | const bid = prototypes.indexOf(bProto); 101 | if (!prototypeChains.hasOwnProperty(bid)) { 102 | computePrototypeChain(bProto); 103 | } 104 | 105 | const id = `${aid},${bid}`; 106 | 107 | operators[op][id] = fn; 108 | 109 | // handle commutative operations automatically 110 | if (commutatives.includes(op) && a !== b) { 111 | // reverse the arguments so that we can deal with any special cases 112 | // involving types that aren't the same 113 | operators[op][`${bid},${aid}`] = (a, b) => fn(b, a); 114 | } else if (op === '<') { 115 | operators['>'][`${bid},${aid}`] = (a, b) => fn(b, a); 116 | } else if (op === '<=') { 117 | operators['>='][`${bid},${aid}`] = (a, b) => fn(b, a); 118 | } else if (op === '==') { 119 | operators['!='][`${aid},${bid}`] = (a, b) => !fn(a, b); 120 | operators['!='][`${bid},${aid}`] = (a, b) => !fn(b, a); 121 | } 122 | }; 123 | 124 | const defineUnaryOperator = function(op, types, fn) { 125 | const [a] = types; 126 | 127 | if (typeof a !== 'function') { 128 | throw new Error('Type must be a function/class'); 129 | } 130 | 131 | const aProto = a.prototype; 132 | 133 | if (aProto === Number.prototype) { 134 | throw new Error(`redefining '${op}' for [Number] is prohibited`); 135 | } 136 | 137 | if (!prototypes.includes(aProto)) { 138 | prototypes.push(aProto); 139 | } 140 | 141 | const id = prototypes.indexOf(aProto); 142 | 143 | if (!prototypeChains.hasOwnProperty(id)) { 144 | computePrototypeChain(aProto); 145 | } 146 | 147 | operators[op][id] = fn; 148 | }; 149 | 150 | const allowedOperators = [ 151 | '|', '^', '&', '~', 152 | '==', '<', '<=', 153 | '<<', '>>', '>>>', 154 | '+', '-', '*', '/', '%' 155 | ]; 156 | 157 | Function.defineOperator = function(op, types, fn) { 158 | if (!allowedOperators.includes(op)) { 159 | throw new Error(`'${op}' cannot be overloaded`); 160 | } 161 | 162 | if (types.length === 2) { 163 | assert(fn.length === 2, 164 | `function takes ${fn.length} params but should take 2`); 165 | return defineBinaryOperator(op, types, fn); 166 | } else if (types.length === 1) { 167 | assert(fn.length === 1, 168 | `function takes ${fn.length} params but should take 1`); 169 | return defineUnaryOperator(op, types, fn); 170 | } 171 | }; 172 | 173 | const operatorData = { 174 | plus: ['+', (a, b) => a + b], 175 | minus: ['-', (a, b) => a - b], 176 | times: ['*', (a, b) => a * b], 177 | divide: ['/', (a, b) => a / b], 178 | remainder: ['%', (a, b) => a / b], 179 | unaryPlus: ['+', (a) => +a], 180 | unaryMinus: ['-', (a) => -a], 181 | 182 | equality: ['==', (a, b) => a == b], 183 | inequality: ['!=', (a, b) => a != b], 184 | lessThan: ['<', (a, b) => a !== b], 185 | lessThanOrEqual: ['<=', (a, b) => a !== b], 186 | greaterThan: ['>', (a, b) => a !== b], 187 | greaterThanOrEqual: ['>=', (a, b) => a !== b], 188 | 189 | shiftLeft: ['<<', (a, b) => a << b], 190 | shiftRight: ['>>', (a, b) => a >> b], 191 | unsignedShiftRight: ['>>>', (a, b) => a >>> b], 192 | bitwiseOr: ['|', (a, b) => a | b], 193 | bitwiseAnd: ['&', (a, b) => a & b], 194 | bitwiseXor: ['^', (a, b) => a ^ b], 195 | bitwiseNot: ['~', (a) => ~a], 196 | 197 | logicalOr: ['||', (a, b) => a || b], 198 | logicalAnd: ['&&', (a, b) => a && b], 199 | }; 200 | 201 | Object.keys(operatorData).forEach(name => { 202 | const op = operatorData[name][0]; 203 | const fn = operatorData[name][1]; 204 | 205 | const id = fn.length === 2 ? '0,0' : '0'; 206 | operators[op][id] = fn; 207 | 208 | const sym = Symbol[name] = Symbol(name); 209 | const objProto = Object.prototype; 210 | 211 | if (fn.length === 2) { 212 | Function[sym] = (a, b) => { 213 | let aProto, bProto; 214 | 215 | if (a != null) { 216 | aProto = Object.getPrototypeOf(a); 217 | if (aProto !== objProto && !prototypes.includes(aProto)) { 218 | computePrototypeChain(aProto); 219 | } 220 | } 221 | 222 | if (b != null) { 223 | bProto = Object.getPrototypeOf(b); 224 | if (bProto !== objProto && !prototypes.includes(bProto)) { 225 | computePrototypeChain(bProto); 226 | } 227 | } 228 | 229 | const aid = a == null ? 0 : prototypes.indexOf(aProto); 230 | const bid = b == null ? 0 : prototypes.indexOf(bProto); 231 | 232 | // optimize for an exact match of the operand prototypes 233 | const fastId = `${aid},${bid}`; 234 | if (operators[op][fastId]) { 235 | const fn = operators[op][fastId]; 236 | return fn(a, b); 237 | } 238 | 239 | // We copy the prototype chains so that we don't modify them. 240 | const chainA = [...prototypeChains[aid]]; 241 | const chainB = [...prototypeChains[bid]]; 242 | 243 | const ids = []; 244 | 245 | // TODO: if the operator is commutative we can simplify this a bit 246 | while (chainA.length > 1 && chainB.length > 1) { 247 | if (chainA.length > chainB.length) { 248 | ids.push(...chainA.map(id => `${id},${chainB[0]}`)); 249 | chainA.shift(); 250 | } else if (chainB.length > chainA.length) { 251 | ids.push(...chainB.map(id => `${chainA[0]},${id}`)); 252 | chainB.shift(); 253 | } else { 254 | ids.push(`${chainA[0]},${chainB[0]}`); 255 | // Ensure the the sum of the chain lengths of each pair of 256 | // prototype chains is monotonically decrease. 257 | for (var i = 1; i < chainA.length; i++) { 258 | ids.push(`${chainA[0]},${chainB[i]}`); 259 | ids.push(`${chainA[i]},${chainB[0]}`); 260 | } 261 | chainA.shift(); 262 | chainB.shift(); 263 | } 264 | } 265 | 266 | // base case 267 | ids.push('0,0'); 268 | 269 | const id = ids.find(id => operators[op][id]); 270 | 271 | const fn = operators[op][id]; 272 | return fn(a, b); 273 | }; 274 | } else { 275 | Function[sym] = (a) => { 276 | if (a != null) { 277 | const aProto = Object.getPrototypeOf(a); 278 | if (aProto !== objProto && !prototypes.includes(aProto)) { 279 | computePrototypeChain(aProto); 280 | } 281 | } 282 | 283 | const aid = prototypes.indexOf(Object.getPrototypeOf(a)); 284 | const id = prototypeChains[aid].find(id => operators[op][id]); 285 | 286 | const fn = operators[op][id]; 287 | return fn(a); 288 | }; 289 | } 290 | }); 291 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require('./define-operator'); 2 | 3 | module.exports = { 4 | operator: require('./decorator'), 5 | }; 6 | -------------------------------------------------------------------------------- /test/complex.js: -------------------------------------------------------------------------------- 1 | function Complex(re, im = 0) { 2 | Object.assign(this, { re, im }); 3 | } 4 | 5 | Function.defineOperator( 6 | '+', 7 | [Complex, Complex], 8 | (a, b) => new Complex(a.re + b.re, a.im + b.im) 9 | ); 10 | 11 | Function.defineOperator( 12 | '+', 13 | [Number, Complex], 14 | (a, b) => new Complex(a + b.re, b.im) 15 | ); 16 | 17 | Function.defineOperator( 18 | '*', 19 | [Complex, Complex], 20 | (a, b) => new Complex(a.re * a.re - (b.im * b.im), a.re * b.im + a.im * b.re) 21 | ); 22 | 23 | Function.defineOperator( 24 | '*', 25 | [Number, Complex], 26 | (a, b) => new Complex(a * b.re, a * b.im) 27 | ); 28 | 29 | module.exports = Complex; 30 | -------------------------------------------------------------------------------- /test/test-spec.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('babel-polyfill'); 3 | require('../src/define-operator'); 4 | 5 | const assert = require('assert'); 6 | 7 | const { run, runWithArgs, compile } = require('./test-util'); 8 | 9 | const Complex = require('./complex'); 10 | 11 | Function.defineOperator('*', [Number, String], (num, str) => str.repeat(num)); 12 | Function.defineOperator('|', [Set, Set], (a, b) => new Set([...a, ...b])); 13 | Function.defineOperator('<=', [Set, Set], (a, b) => [...a].every((item) => b.has(item))); 14 | Function.defineOperator('<', [Set, Set], (a, b) => a.size < b.size && [...a].every((item) => b.has(item))); 15 | Function.defineOperator('==', [Array, Array], (a, b) => { 16 | if (a.length !== b.length) { 17 | return false; 18 | } else { 19 | for (var i = 0; i < a.length; i++) { 20 | if (a[i] !== b[i]) { 21 | return false; 22 | } 23 | } 24 | } 25 | return true; 26 | }); 27 | 28 | describe('Operator Overloading', () => { 29 | it(`file level with "use overloading" directive`, () => { 30 | const result = run(` 31 | "use overloading"; 32 | 33 | const result = 3 * "foo"; 34 | `); 35 | 36 | assert.equal(result, "foofoofoo"); 37 | }); 38 | 39 | it(`file level without "use overloading" directive`, () => { 40 | const result = run(` 41 | const result = 3 * "foo"; 42 | `); 43 | 44 | assert.equal(typeof result, 'number'); 45 | assert(isNaN(result)); 46 | }); 47 | 48 | it(`function level with and without "use overloading" directive`, () => { 49 | const result = run(` 50 | const result = {}; 51 | 52 | (function() { 53 | "use overloading"; 54 | 55 | result.with = 3 * "foo"; 56 | })(); 57 | 58 | if (true) { 59 | result.without = 3 * "foo"; 60 | } 61 | `); 62 | 63 | assert.equal(result.with, "foofoofoo"); 64 | 65 | assert.equal(typeof result.without, 'number'); 66 | assert(isNaN(result.without)); 67 | }); 68 | 69 | it('handles default operations on numbers', () => { 70 | const result = run(` 71 | "use overloading"; 72 | 73 | const result = { 74 | plus: 10 + 5, 75 | minus: 10 - 5, 76 | times: 10 * 5, 77 | divide: 10 / 5 78 | }; 79 | `); 80 | 81 | assert.deepEqual(result, { 82 | plus: 15, 83 | minus: 5, 84 | times: 50, 85 | divide: 2, 86 | }); 87 | }); 88 | 89 | it('handles default operators on strings', () => { 90 | const result = run(` 91 | "use overloading"; 92 | 93 | const result = 'foo' + 'bar'; 94 | `); 95 | 96 | assert.deepEqual(result, 'foobar'); 97 | }); 98 | 99 | it('handles subclasses defined after the operator has been overloaded on the parent class', () => { 100 | const result = run(` 101 | "use overloading"; 102 | 103 | class SuperSet extends Set { 104 | toString() { 105 | return [...this].join(', '); 106 | } 107 | } 108 | 109 | const a = new SuperSet([1,2]); 110 | const b = new SuperSet([2,3]); 111 | 112 | const result = a | b; 113 | `); 114 | 115 | assert.deepEqual([...result], [1, 2, 3]); 116 | }); 117 | 118 | it('ignores instanceof, in, typeof, etc. operators', () => { 119 | const code = compile(` 120 | const a = typeof "foo"; 121 | const b = "x" in { x: 5, y: 10 }; 122 | const c = "foo" instanceof String; 123 | `); 124 | 125 | assert(code.includes('const a = typeof "foo";')); 126 | assert(code.includes('const b = "x" in { x: 5, y: 10 };')); 127 | assert(code.includes('const c = "foo" instanceof String;')); 128 | }); 129 | 130 | it('&&, ||, and ! cannot be overloaded', () => { 131 | assert.throws(() => { 132 | Function.defineOperator('&&', [Set, Set], (a, b) => {}); 133 | }); 134 | 135 | assert.throws(() => { 136 | Function.defineOperator('||', [Set, Set], (a, b) => {}); 137 | }); 138 | 139 | assert.throws(() => { 140 | Function.defineOperator('!', [Set, Set], (a, b) => {}); 141 | }); 142 | }); 143 | 144 | it('X != Y <=> !(X == Y) holds', () => { 145 | const result = run(` 146 | "use overloading"; 147 | 148 | const a = [1,2,3]; 149 | const b = [1,2,3]; 150 | const c = [4,5,6]; 151 | 152 | const result = { 153 | abEqual: a == b, 154 | abNotEqual: a != b, 155 | bcEqual: b == c, 156 | bcNotEqual: b != c, 157 | }; 158 | `); 159 | 160 | assert.deepEqual(result, { 161 | abEqual: true, 162 | abNotEqual: false, 163 | bcEqual: false, 164 | bcNotEqual: true, 165 | }); 166 | }); 167 | 168 | it('reverses commutable operators', () => { 169 | const result = runWithArgs(` 170 | "use overloading"; 171 | 172 | const z = new Complex(1, -2); 173 | const a = 3; 174 | 175 | const result = { 176 | azProd: a * z, 177 | zaProd: z * a, 178 | azSum: a + z, 179 | zaSum: z + a, 180 | }; 181 | `, ['Complex'], [Complex]); 182 | 183 | assert.deepEqual(result, { 184 | azProd: { re: 3, im: -6 }, 185 | zaProd: { re: 3, im: -6 }, 186 | azSum: { re: 4, im: -2 }, 187 | zaSum: { re: 4, im: -2 }, 188 | }); 189 | }); 190 | 191 | it('A > B <=> B < A', () => { 192 | const result = run(` 193 | "use overloading"; 194 | 195 | const a = new Set([1,2,3]); 196 | const b = new Set([1,2,3]); 197 | const c = new Set([1,2]); 198 | 199 | const result = { 200 | abStrictSubset: a < b, 201 | abStrictSuperset: a > b, 202 | bcStrictSubset: b < c, 203 | bcStrictSuperset: b > c 204 | }; 205 | `); 206 | 207 | assert.deepEqual(result, { 208 | abStrictSubset: false, 209 | abStrictSuperset: false, 210 | bcStrictSubset: false, 211 | bcStrictSuperset: true, 212 | }); 213 | }); 214 | 215 | it('A >= B <=> B <= A', () => { 216 | const result = run(` 217 | "use overloading"; 218 | 219 | const a = new Set([1,2,3]); 220 | const b = new Set([1,2,3]); 221 | const c = new Set([1,2]); 222 | 223 | const result = { 224 | abSubset: a <= b, 225 | abSuperset: a >= b, 226 | bcSubset: b <= c, 227 | bcSuperset: b >= c 228 | }; 229 | `); 230 | 231 | assert.deepEqual(result, { 232 | abSubset: true, 233 | abSuperset: true, 234 | bcSubset: false, 235 | bcSuperset: true, 236 | }); 237 | }); 238 | 239 | it('should handle defining operations on Objects', () => { 240 | Function.defineOperator('+', [Object, Object], (a, b) => 42); 241 | 242 | const result = run(` 243 | "use overloading"; 244 | 245 | const result = {} + {}; 246 | `); 247 | 248 | assert.equal(result, 42); 249 | 250 | // cleanup so we don't affect other tests 251 | Function.defineOperator('+', [Object, Object], (a, b) => a + b); 252 | }); 253 | 254 | it('should handle operations on null', () => { 255 | const result = run(` 256 | "use overloading"; 257 | 258 | const result = null + 1; 259 | `); 260 | 261 | assert.equal(result, 1); 262 | }); 263 | 264 | it('should handle operations on undefined', () => { 265 | const result = run(` 266 | "use overloading"; 267 | 268 | const result = undefined + 1; 269 | `); 270 | 271 | assert(typeof result === 'number'); 272 | assert(isNaN(result)); 273 | }); 274 | 275 | it('overloading operators for [Number, Number] is prohibited', () => { 276 | assert.throws(() => { 277 | run(` 278 | "use overloading"; 279 | 280 | Function.defineOperator('+', [Number, Number], (a, b) => a - b); 281 | 282 | const result = 1 + 2; 283 | `); 284 | }); 285 | }); 286 | 287 | it('overloading unary operators for [Number] is prohibited', () => { 288 | assert.throws(() => { 289 | run(` 290 | "use overloading"; 291 | 292 | Function.defineOperator('+', [Number], (a) => -a); 293 | 294 | const result = +2; 295 | `); 296 | }); 297 | 298 | assert.throws(() => { 299 | run(` 300 | "use overloading"; 301 | 302 | Function.defineOperator('-', [Number], (a) => +a); 303 | 304 | const result = -2; 305 | `); 306 | }); 307 | }); 308 | 309 | it(`overloading '+' for [String, String] is prohibited`, () => { 310 | assert.throws(() => { 311 | run(` 312 | "use overloading"; 313 | 314 | Function.defineOperator('+', [String, String], (a, b) => a - b); 315 | 316 | const result = 1 + 2; 317 | `); 318 | }); 319 | }); 320 | 321 | it(`overloading '&&' or '||' for [Boolean, Boolean] is prohibited`, () => { 322 | assert.throws(() => { 323 | run(` 324 | "use overloading"; 325 | 326 | Function.defineOperator('&&', [Boolean, Boolean], (a, b) => a + b); 327 | 328 | const result = true && false; 329 | `); 330 | }); 331 | 332 | assert.throws(() => { 333 | run(` 334 | "use overloading"; 335 | 336 | Function.defineOperator('||', [Boolean, Boolean], (a, b) => a + b); 337 | 338 | const result = true || false; 339 | `); 340 | }); 341 | }); 342 | }); 343 | -------------------------------------------------------------------------------- /test/test-util.js: -------------------------------------------------------------------------------- 1 | const babel = require('babel-core'); 2 | 3 | const run = (code) => { 4 | const fn = Function(babel.transform(code, { 5 | presets: ["es2015", "stage-1"], 6 | plugins: ["babel-plugin-operator-overloading"] 7 | }).code + '\nreturn result;'); 8 | 9 | return fn(); 10 | }; 11 | 12 | const runWithArgs = (code, names, values) => { 13 | const fn = Function(...names, babel.transform(code, { 14 | presets: ["es2015", "stage-1"], 15 | plugins: ["babel-plugin-operator-overloading"] 16 | }).code + '\nreturn result;'); 17 | 18 | return fn(...values); 19 | }; 20 | 21 | const compile = (code) => 22 | babel.transform(code, { 23 | plugins: ["babel-plugin-operator-overloading"] 24 | }).code; 25 | 26 | module.exports = { run, runWithArgs, compile }; 27 | --------------------------------------------------------------------------------