├── LANGCOMP.md ├── LICENSE ├── PROTOSPEC.md ├── README.md └── src ├── README.md ├── shim ├── .babelrc ├── .eslintrc.json ├── README.md ├── package.json ├── shim.js └── shim.spec.js └── transform ├── README.md ├── package.json ├── plugin.js └── plugin.spec.js /LANGCOMP.md: -------------------------------------------------------------------------------- 1 | 2 | ## Cross-language comparison 3 | 4 | Most programming lanuguage designers have to consider operator semantics at some point, so we have a lot to refer to when considering the choices other languages have made. 5 | 6 | ### Which operators support overloading? 7 | 8 | #### A static set of operators are overloadable 9 | 10 | Languages: Python, Ruby, Lua, [Matlab](https://www.mathworks.com/help/matlab/matlab_oop/implementing-operators-for-your-class.html), Kotlin, C#, Rust ([considered and rejected](https://github.com/rust-lang/rfcs/issues/818)), [AssemblyScript](https://github.com/AssemblyScript/assemblyscript/blob/master/tests/compiler/std/operator-overloading.ts) 11 | 12 | This seems to be the most common pattern. Within this, it's also common to define some operators in terms of others, as this proposal does. 13 | 14 | #### Operators just don't have special syntax 15 | 16 | Languages: Common Lisp, Smalltalk, Self, Slate, Factor, Forth 17 | 18 | - In the Lisp lineage (Common Lisp, Clojure, Scheme, Racket), code is written with prefix syntax. Function names can traditionally include operator characters. 19 | - In the Forth lineage (Factor, Forth), operators are post-fix functions; tokens are typically delimited by spaces, so a function name can include punctuation/operator characters. 20 | - In the Smalltalk lineage (Smalltalk, Self, Slate), operators are left-associative, all have the same precedence, and a message which begins with an operator-like character can omit the `:` and is always taken to have just one argument. 21 | 22 | #### Predictable precedence, user-defined operators 23 | 24 | Languages: OCaml, Scala, R 25 | 26 | [OCaml operator precedence](https://caml.inria.fr/pub/docs/manual-ocaml/expr.html) is based on the initial character(s) of the operator. 27 | 28 | R doesn't have operator overloading, but it lets programmers define their own operators in between `%`, e.g., `%abc%`, which are then treated as functions. Haskell has a related facility, where any function `f` can be used infix by enclosing it in backticks. 29 | 30 | #### User-defined operator precedence 31 | 32 | Languages: Haskell, Swift, Prolog 33 | 34 | These can be especially difficult to parse! User-defined operator precedence is "anti-modular" since you need to import the precedence and associativity declarations of the imported modules in order to be able to even parse a particular module. Sometimes this ends up requiring multiple passes over the module graph in a practical implementation. 35 | 36 | #### Takeaways 37 | 38 | Allowing the built-in operators to be overloaded, and not supporting user-defined operators, is a middle-of-the-road, common design. The cross-language comparison validates this proposal's conservative choice, as user-defined operators cause issues in parsing and readability. 39 | 40 | ### Dispatch mechanism for operators 41 | 42 | #### Operators are just functions, with no dispatch 43 | 44 | Languages: ML, Forth, R (user-defined operators) 45 | 46 | These languages may have operator syntax, but they just operate on a fixed type. In ML, the pattern is to use differnet operators for different types: For example, `+` for integers and `+.` for floats. 47 | 48 | #### Built-in, fixed numeric tower 49 | 50 | Languages: Java, C, Scheme, Factor, R (built-in operators), Clojure, Common Lisp, [Go](https://golang.org/doc/faq#overloading), PHP ([RFC](https://wiki.php.net/rfc/operator-overloading) apparently not yet in effect) 51 | 52 | The choice to omit overloading is often a deliberate design decision. Excluding operator overloading and using a dependable set of built-in types is explained to enhance the predictability of code. 53 | 54 | #### Static dispatch 55 | 56 | Languages: C++, Swift 57 | 58 | C++ overloads operators in a static way, with logic similar to its function type overloading mechanism. See [isocpp's FAQ](https://isocpp.org/wiki/faq/operator-overloading) for details. 59 | 60 | [Swift operators](https://docs.swift.org/swift-book/LanguageGuide/AdvancedOperators.html) can be used as part of simple overloading (as in the examples in that page), or can be defined as part of a [protocol](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html). 61 | 62 | #### Dispatch based on the left operand 63 | 64 | Languages: Smalltalk, Self, Ruby 65 | 66 | Languages in the Smalltalk lineage (including Smalltalk, Self and Ruby) send the operator to the receiver directly as an ordinary message send. 67 | 68 | #### Check the left operand, then the right, for possible overloads 69 | 70 | Languages: Python, Lua 71 | 72 | For `+`, Python checks for an `__add__` method on the left operand, and then an `__radd__` method on the right operand. Similarly, for `+`, Lua will look in the metatable of the left operand for `__add`, and if it's missing, look in the metadatable for the right operand. 73 | 74 | This enables useful patterns like multiplying a number by a vector, with the vector as the right operand, and the one that defines the overload. 75 | 76 | However, it's hard to imagine how to implement this without significant performance overhead, and how to define it in a way that puts built-in types on a level playing field with user-defined objects with operator overloading. 77 | 78 | #### Single dispatch based on both operands 79 | 80 | Languages: [Haskell](http://hackage.haskell.org/package/base-4.12.0.0/docs/Prelude.html#t:Num), [Rust](https://doc.rust-lang.org/std/ops/index.html) 81 | 82 | These langauges each have a way to define an interface which specifies that a method should have the receiver and an argument of the same type (or, in Rust's case, a concrete subclass can specify a type parameter for the second operand to override the default of being the same as the left operand). This technique is used in the definition of operators. 83 | 84 | #### Full multimethods for operators 85 | 86 | The only language I know of that works like this is Slate. See [the classic Slate paper](http://www.cs.cmu.edu/~aldrich/papers/ecoop05pmd.pdf) for more details on Slate's prototype multiple dispatch mechanism. 87 | 88 | Common Lisp and Closure support multimethods, but the built-in arithmetic functions are *not* user-extendable multimethods. Instead, they have a fixed numeric tower. 89 | 90 | Prototype multiple dispatch seems suboptimal because it requires a complex traversal of the inheritance hierarchy, and use cases are unclear. The Slate paper suggests dynamically changing the prototype as an object moves between "roles", which is not a popular idiom for that purposes in JavaScript as far as I know. 91 | 92 | #### Call a method on the higher-precedence operand 93 | 94 | Matlab's ["precedence"](https://www.mathworks.com/help/matlab/matlab_oop/object-precedence-in-expressions-using-operators.html) system handles operators by determining which operand has lower precedence, and calling the method on the operand with higher precedence. Precedence is indicated with the InferiorClasses property, rather than implicitly based on execution order as in this proposal. 95 | 96 | #### Takeaways 97 | 98 | The proposal here is most similar to Matlab semantics, and differs from operator dispatch in most object-oriented programming languages. We have good reasons here for not using property access for operator overloading, but we should proceed with caution. 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Daniel Ehrenberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PROTOSPEC.md: -------------------------------------------------------------------------------- 1 | ## Mechanism 2 | 3 | The operator overloading proposal is based on creation of objects with an `[[OperatorSet]]` internal slot, which points to a spec-internal Operator Set record. The Operator Set controls the dispatch of operators. A functional and decorator-based interface exists to define Operator Sets (called `Operators` in JavaScript) and create objects that have operators dispatched according to them. 4 | 5 | ### Operator Sets 6 | 7 | The global counter OperatorCounter is used to assign an integer to each Operator Set. 8 | 9 | An Operator Definitition Table is a table with values for each [numeric type operation](https://tc39.github.io/proposal-bigint/#table-numeric-type-ops), as enumerated in the BigInt specification, either JavaScript function objects or the `~empty~` sentinel value. A "binary Operator Definition Table" has `~empty~` as its value for each unary operation. Note that the sameValue and sameValueZero operations from that table are never invoked through operator overloading. 10 | 11 | Each Operator Set record has the following fields, none of which are ever modified after the operator set is created: 12 | 13 | | Name | Type | Description | 14 | |------|------|-------------| 15 | | `[[OperatorCounter]]` | integer | Value of the OperatorCounter when this Operator Set was created | 16 | | `[[SelfOperatorDefinition]]` | Operator Definition Table | Definition of unary operators, and binary operators with both operands being of this Operator Set | 17 | | `[[LeftOperatorDefinitions]]` | List of length `[[OperatorCounter]]` with elements either `~empty~` or binary Operator Defintion Table | Operator definitions for when this is the left operand, and something of a lower OperatorCounter is the right operand | 18 | | `[[RightOperatorDefinitions]]` | "" | "" but for the right operand | 19 | | `[[OpenOperators]]` | List of Strings representing operators | The set of operators that this type is open to having overloaded by a yet-to-be-defined type, as the other operand | 20 | 21 | Built-in Operator Sets exist for the built-in numeric primitive types: String, BigInt and Number. The phrase "the Operator Set of `x`" refers to `x.[[OperatorSet]]` if `x` is an object, and the built-in Operator Set for those four types, which describes the currently specified behavior. 22 | 23 | Note, String overloading only supports the == and < operators, and not the other "numeric" operators. Boolean and Symbol may not have operators overloaded on them. 24 | 25 | ### Operator usage semantics 26 | 27 | A few operators are special, and the rest follow a pattern. 28 | 29 | #### Shared algorithms 30 | 31 | The operation ToOperand(arg [, hint]) does the following: 32 | 1. If arg is an Object with an `[[OperatorSet]]` internal slot, 33 | 1. If the operator set of arg is not in the allowed set of operators, which had a `with operators from` declaration, based on the lexical scope, throw a TypeError. 34 | 1. Otherwise, return arg. 35 | 1. Otherwise, return ToPrimitive(arg [, hint]). 36 | 37 | DispatchBinaryOperator(operator, a, b): 38 | 1. If the operator set of a and b are the same, 39 | 1. If the common operator set does not have a definition for the operator in its `[[SelfOperatorDefinition]]` table, throw a TypeError. 40 | 1. Otherwise, apply the definition to the arguments and return the result. 41 | 1. Otherwise, if a's operator set has a lower `[[OperatorCounter]]` than b's operator set, 42 | 1. Find the relevant operator definition table in the a.[[OperatorSet]].[[OperatorCounter]]'th element of b.[[OperatorSet]].[[RightOperatorDefinitions]]. 43 | 1. If the operator is `~empty~` in that operator definition table, throw a TypeError. 44 | 1. Otherwise, apply the operator to the arguments using the operator table. 45 | 1. Otherwise, b's operator set has a lower `[[OperatorCounter]]` than a's operator set, 46 | 1. Perform the instructions for the corresponding case, but referencing the `[[LeftOperatorDefinitions]]` instead of `[[RightOperatorDefinitions]]`. 47 | 48 | DispatchUnaryOperator(operator, arg): 49 | 1. If the Operator Set of value doesn't have a definition for the operator, throw a TypeError. 50 | 1. Otherwise, call the operator on value and return the result. 51 | 52 | #### Special operators 53 | 54 | The definition of `+`(a, b): 55 | 1. Set a to ToOperand(a). 56 | 1. Set b to ToOperand(b). 57 | 1. If Type(a) is String or Type(b) is String, 58 | 1. Return the string-concatenation of ToString(a) and ToString(b). 59 | 1. Return DispatchBinaryOperator(a, b). 60 | 61 | The definition of `==`(x, y): 62 | 1. If Type(x) is the same as Type(y), and neither x nor y are Objects with an `[[OperatorSet]]` internal slot, then 63 | 1. Return the result of performing Strict Equality Comparison x === y. 64 | 1. If x is null and y is undefined, return true. 65 | 1. If x is undefined and y is null, return true. 66 | 1. If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y. 67 | 1. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y). 68 | 1. If Type(x) is Object and x does not have an [[OperatorSet]] internal slot, set x to ToPrimitive(x). 69 | 1. If Type(y) is Object and y does not have an [[OperatorSet]] internal slot, set y to ToPrimitive(y). 70 | 1. If a === b is *true*, return *true*. 71 | 1. Return DispatchBinaryOperator('==', a, b), but return *false* on a missing method rather than throwing a TypeError. 72 | 73 | The definition of [Abstract Relational Comparison](https://tc39.github.io/ecma262/#sec-abstract-relational-comparison), which ends up defining <, <=, >, >=: 74 | 1. Set a to ToOperand(a, hint Number), and set b to ToOperand(b, hint Number), in the order driven by their order in code. 75 | 1. If a and b are both strings, follow the current logic for comparing two strings. 76 | 1. Otherwise, return DispatchBinaryOperator('<', a, b). 77 | 78 | Note that String can only be overloaded for the above operators, and cannot be usefully overloaded for the below "numerical" operators. 79 | 80 | #### Numerical operators 81 | 82 | The operation ToNumericOperand(arg) is used in the following definitions: 83 | 1. If Type(arg) is Number or Type(arg) is BigInt, return arg. 84 | 1. If Type(arg) is Object and arg has a `[[OperatorSet]]` internal slot, return ToOperand(arg). NOTE: The ToOperand call is just to check whether it was declared as `with operators from`. 85 | 1. Return ToNumeric(arg). NOTE: This step may only convert to one of the built-in numeric types; value types may define more. 86 | 87 | For a unary operator (such as unary `-`, `++`, `--`, `~`) applied to `arg`: 88 | 1. Let value be ToNumericOperand(arg). 89 | 1. Return DispatchUnaryOperator(operator, arg). 90 | 91 | For a binary operator which is not listed above (such as `*`, `/`, `<`) applied to `a` and `b`: 92 | 1. Set a to ToNumericOperand(a). 93 | 1. Set b to ToNumericOperand(b). 94 | 1. Return DispatchBinaryOperator(operator, a, b). 95 | 96 | #### Integer-indexed property access 97 | 98 | If the `[[SelfOperatorDefinition]]` record contains a definition for `[]` or `[]=`, then the constructor creates an exotic object, with semantics similar to TypedArrays' [Integer-Indexed Exotic Objects](https://tc39.github.io/ecma262/#sec-integer-indexed-exotic-objects), with the following substitutions: 99 | - Calls out to the `[]` function replace IntegerIndexedElementGet 100 | - Calls to the `[]=` function replace IntegerIndexedElementSet 101 | - Get of the `length` property replaces reads of `[[ArrayLength]]` (only used in HasProperty) 102 | - For HasProperty, the detached check is omitted (this means that TypedArrays can't be perfectly emulated) 103 | - In DefineOwnProperty, the length check is skipped (to be handled by `[]=`) 104 | 105 | If neither `[]` nor `[]=` are overloaded, then an ordinary object is created. 106 | 107 | ### Functional definition interface 108 | 109 | The `Operators` object (which could be exposed from a [built-in module](https://github.com/tc39/proposal-javascript-standard-library/)) can be called as a function. Like arrow functions, it is not constructable. It is passed a variable number of arguments. The first argument is translates into the `[[SelfOperatorDefinition]]`, while subsequent arguments are individual entries in the `[[LeftOperatorDefinitions]]` or `[[RightOperatorDefinitions]]` lists (based on any `left:` or `right:` property they have). When defining operators which are operating between two different types, the `[[OpenOperators]]` field of that other operator set will be consulted. 110 | 111 | ### Decorator definition interface 112 | 113 | The decorator interface provides convenient syntactic sugar over the functional interface above. 114 | 115 | The `Operators` object has two properties which are decorators: 116 | - `Operators.define`, a method decorator, which does the following: 117 | 1. Returns the method as is, adding a finisher which closes over the method and the arguments to the decorator. 118 | 1. The finisher appends a tuple containing the method function and the other arguments to a List, which is "associated with" the class. 119 | - `Operators.overloaded`, a class decorator, which does the following: 120 | 1. Add a finisher to do the following things: 121 | 1. Assert that the superclass is Operators (which will just throw if called as a super constructor) 122 | 1. Take the associated define list and munge it into the argument for the functional definition interface. 123 | 1. Call into the functional definition interface, and replace the superclass with the result of that call. 124 | 1. Prevent changing the superclass, e.g. through Object.preventExtensions. 125 | 126 | ### `with operators from` declarations 127 | 128 | The purpose of these declarations is to meet this goal: 129 | 130 | > It should not be possible to change the behavior of existing code using operators by unexpectedly passing it an object which overloads operators. 131 | 132 | The idea of these declarations is, within a particular chunk of code, only explicitly enabled operator overloads should be used. The `with operators from` declaration specifically enables certain types for overloading, leaving the rest prohibited. 133 | 134 | When such a declaration is reached, the argument `arg` has Get(arg, `Operators.operators`) applied, to get the related Operators instance, and then the `[[OperatorSetDefinition]]` internal slot of that is read. If that doesn't exist, a TypeError is thrown. If it does, then the relevant operator set is added to the lexical scope for permitted use. 135 | 136 | It's not clear whether the performance (see below) or ergonomics impact will outweigh the predictability/security benefits of this check; more thought and discussion is needed. 137 | 138 | ### Implementation notes 139 | 140 | *Warning: Wild speculation follows; others will have more realistic input for operator overloading* 141 | 142 | The implementation experience of BigInt showed that it wasn't hard to add an extra "else case" to existing operators while avoiding slowing down existing code. The hard part will be ensuring that operator overloading is cheap when it's used. 143 | 144 | The implementation can take advantage of the fact that the operators are something overloaded in the base class. In V8, for example, an object with overloaded operators might be representable as its own `instance_type` (one `instance_type` for all objects with overloaded operators), making it cheap to check whether operators are overloaded at all. (BigInt is also distinguished using the `instance_type`.) 145 | 146 | The actual link to the operators can be found with `GetConstructor()`, avoiding extra memory overhead on the map (at the expense of finding those operators requiring a traversal). There does not need to be any logic to invalidate the found operators (`PrototypeInfo`-style), as they cannot be changed. An object never transitions between having operators overloaded and not, and never changes which operator overloadings apply to it. 147 | 148 | Within code generated by a JIT, or an inline cache, a map check of both operands should be sufficient to dispatch to the same operator implementation. No invalidation logic is needed, as long as it's the same map. 149 | 150 | To include the checks about whether operators are currently in use, an extra check will be required, and it will be hard to optimize away this check. A single `with operators` declaration can run multiple times with different values running through it, and this must be handled properly. The idea would be to include the mask of permitted operator indices as an implicit lexically scoped variable which contains a bitmask, and check whether the arguments have those indices enabled before each overloaded operation. This mask will never change after being initialized. An inline cache can reduce the overhead of the check by checking that the identity of the bitmask object is the same as what was expected, which will imply that its contents are the same. It's possible that the overhead will be considered too great here to do the extra checks in practice. 151 | 152 | For objects overloading `[]` and `[]=`, a special V8 ElementsKind could be used to keep track of this special behavior. 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Operator overloading in JavaScript 2 | 3 | Should JavaScript support operator overloading? It's not clear one way or another whether operator overloading has enough benefit in terms of the complexity, in terms of language design, implementation work, and security surface. At the same time, there's an argument that it's better to provide general operator overloading than to overload operators for specific, additional, built-in constructs (and partly for this reason, Decimal was not added as part of ES6, although it might have been useful for JS developers). 4 | 5 | This article tries to examine how operator overloading *could* look, if we want to go in this direction. Hopefully, the concreteness will help us decide whether to go down this path, which can help move the committee towards concrete next steps on long-standing feature requests, one way or another. 6 | 7 | Status: **Withdrawn** 8 | 9 | ## Case studies 10 | 11 | Operator overloading is all about enabling richer libraries. This section gives four motivating use cases of such rich libraries. 12 | 13 | ### Numeric types 14 | 15 | JavaScript has a very restricted set of numeric types. Traditionally, it had just Number: an IEEE-754 double-precision binary float. The Stage 4 [BigInt proposal](http://github.com/tc39/proposal-bigint/) added a single new numeric type for arbitrary-size integers. But there are more numeric types that developers need in practice, such as decimals, rationals, complex numbers, etc. Operator overloading can provide these, with intuitive syntax for their use. 16 | 17 | ```mjs 18 | // Usage example 19 | import Decimal from "./decimal.mjs"; 20 | with operators from Decimal; // Enable operator overloading for decimals 21 | // Declaration may use some other syntax 22 | 23 | Decimal(1) + Decimal(2) // ==> Decimal(3) 24 | Decimal(3) * Decimal(2) // ==> Decimal(6) 25 | Decimal(1) == Decimal(1) // ==> true 26 | Decimal(1) == 1 // ==> true 27 | 1 == Decimal(1) // ==> true 28 | Decimal(1) === 1 // ==> false (not overloadable) 29 | ``` 30 | 31 | A possible implementation of this module: 32 | 33 | ```mjs 34 | // ------------- 35 | // decimal.mjs 36 | 37 | import Big from './big.mjs'; // https://github.com/MikeMcl/big.js/ 38 | 39 | const DecimalOperators = Operators({ 40 | "+"(a, b) { return a._big.plus(b._big); }, 41 | "*"(a, b) { return a._big.times(b._big); }, 42 | "=="(a, b) { return a._big.eq(b._big); }, 43 | }, { 44 | left: Number, { 45 | "=="(a, b) { return b._big.eq(a); } 46 | } 47 | }, { 48 | right: Number, { 49 | "=="(a, b) { return a._big.eq(b); } 50 | } 51 | }); 52 | 53 | export default 54 | class Decimal extends DecimalOperators { 55 | _big; 56 | constructor(arg) { this._big = new Big(arg); } 57 | } 58 | Object.preventExtensions(Decimal); // ensure the operators don't change 59 | ``` 60 | 61 | ### Matrix/vector computations 62 | 63 | JavaScript is increasingly used for data processing and analysis, with libraries like [stdlib](https://stdlib.io/). These calculations are made a bit more awkward because things involving vector, matrix and tensor calculations need to be done all via method chaining, rather than more naturally using operators as they can in many other programming languages. Operator overloading could provide that natural phrasing. 64 | 65 | ```mjs 66 | // Usage example 67 | import { Vector } from "./vector.mjs"; 68 | with operators from Vector; 69 | 70 | new Vector([1, 2, 3]) + new Vector([4, 5, 6]) // ==> new Vector([5, 7, 9]) 71 | 3 * new Vector([1, 2, 3]) // ==> new Vector([3, 6, 9]) 72 | new Vector([1, 2, 3]) == new Vector([1, 2, 3]) // ==> true 73 | (new Vector([1, 2, 3]))[1] // ==> 2 74 | ``` 75 | 76 | A possible implementation: 77 | 78 | ```mjs 79 | // ---------------- 80 | // vector.mjs 81 | // This example uses the "Imperative API". 82 | // It would also be possible to write with the decorator-based API of the previous example. 83 | 84 | const VectorOps = Operators({ 85 | "+"(a, b) { 86 | return new Vector(a.contents.map((elt, i) => elt + b.contents[i])); 87 | }, 88 | "=="(a, b) { 89 | return a.contents.length === b.contents.length && 90 | a.contents.every((elt, i) => elt == b.contents[i]); 91 | }, 92 | "[]"(a, b) { 93 | return a.contents[b]; 94 | } 95 | }, { 96 | left: Number, 97 | "*"(a, b) { 98 | return new Vector(b.contents.map(elt => elt * a)); 99 | } 100 | }); 101 | 102 | export class Vector extends VectorOps { 103 | contents; 104 | constructor(contents) { 105 | super(); 106 | this.contents = contents; 107 | } 108 | get length() { return this.contents.length; } 109 | } 110 | Object.preventExtensions(Vector); // ensure the operators don't change 111 | ``` 112 | 113 | ### Equation DSLs 114 | 115 | JavaScript is used in systems with equation-based DSLs, such as [TensorFlow.js](https://js.tensorflow.org/). In systems like TensorFlow, operators can be used to construct an abstract formula in other programming languages. Operator overloading could allow these formula DSLs to be expressed as infix expressions, as people naturally think of them. 116 | 117 | For example, in TensorFlow.js's introductory tutorials, there is an [example](https://github.com/tensorflow/tfjs-examples/blob/master/polynomial-regression-core/index.js#L63) of an equation definition as follows: 118 | ```js 119 | function predict(x) { 120 | // y = a * x ^ 3 + b * x ^ 2 + c * x + d 121 | return tf.tidy(() => { 122 | return a.mul(x.pow(tf.scalar(3, 'int32'))) 123 | .add(b.mul(x.square())) 124 | .add(c.mul(x)) 125 | .add(d); 126 | }); 127 | } 128 | ``` 129 | 130 | It's unfortunate that the equation has to be written twice, once to explain it and once to write it in code. With operator overloading and extensible literals, it might be written as follows instead: 131 | 132 | ```js 133 | function predict(x) { 134 | with operators from tf.equation; 135 | 136 | // y = a * x ^ 3 + b * x ^ 2 + c * x + d 137 | return tf.tidy(() => { 138 | return a * x ** tf.scalar(3, 'int32') 139 | + b * x.square() 140 | + c * x 141 | + d; 142 | }); 143 | } 144 | ``` 145 | 146 | At this point, maybe you don't even need that comment! 147 | 148 | ### Ergonomic CSS units calculations 149 | 150 | Tab Atkins [proposed](https://www.xanthir.com/b4UD0) that CSS support syntax in JavaScript for CSS unit literals and operators. The [CSS Typed OM](https://drafts.css-houdini.org/css-typed-om-1/) turned out a bit different, with ergonomic affordances but without using new types of literals or operator overloading. With this proposal, in conjunction with [extended numeric literals](https://github.com/tc39/proposal-extended-numeric-literals), we could have some more intuitive units calculations than the current function- and method-based solution. 151 | 152 | In this case, the CSSNumericValue platform objects would come with operator overloading already enabled. Their definition in the CSS Typed OM specification would, indirectly, make use of the same JavaScript mechanism that 153 | 154 | ```js 155 | with operators from CSSNumericValue; 156 | 157 | document.querySelector("#element").style.paddingLeft = Css.em(3) + CSS.px(2); 158 | ``` 159 | 160 | ## Design goals 161 | 162 | - Expressivity 163 | - Support operator overloading on both mutable and immutable objects, and in the 164 | future, typed objects and value types. 165 | - Support operands of different types and the same type, as in the above examples. 166 | - Explain all of JS's behavior on existing types in terms of operator overloading. 167 | - Available in both strict and sloppy mode, with and without class syntax. 168 | - Predictability 169 | - The meaning of operators on existing objects shouldn't be overridable 170 | or monkey-patchable, both for built-in types and for objects defined in 171 | other libraries. 172 | - It should not be possible to change the behavior of existing code 173 | using operators by unexpectedly passing it an object which overloads operators. (*If this is feasible.*) 174 | - Don't encourage a crazy coding style in the ecosystem. 175 | - Efficiently implementable 176 | - In native implementations, don't slow down code which doesn't take advantage of operator overloading (including within a module that uses operator overloading in some other paths). 177 | - When operator overloading is used, it should lend itself to relatively efficient native implementations, including 178 | - In the startup path, when code is run just a few times 179 | - Lends itself well to inline caching (for both monomorphic and polymorphic cases) to reduce any overhead of the dispatch 180 | - Feasible to optimize in a JIT (for both monomorphic and polymorphic cases), with a minimal number of cheap hidden class checks, and without extremely complicated cases for when things become invalid 181 | - Don't create too much complexity in the implementation to support such performance 182 | - When enough type declarations are present, it should be feasible to implement efficiently in TypeScript, similarly to BigInt's implementation. 183 | - Operator overloading should be a way of 'explaining the language' and providing hooks into something that's already there, rather than adding something which is a very different pattern from built-in operator definitions. 184 | 185 | ### Avoiding classic pitfalls of operator overloading and readability 186 | 187 | The accusation is frequently made at C++ and Haskell that they are unreadable due to excessive use of obscure operators. On the other hand, in the Python ecosystem, operators are generally considered to make code significantly cleaner, e.g., in their use in NumPy. 188 | 189 | This proposal includes several subtle design decisions to nudge the ecosystem in a direction of not using operator overloading in an excessive way, while still supporting the motivating case studies: 190 | - Operators can be overloaded for one operand being a new user-defined type and the other operand being a previously defined type only in certain circumstances: 191 | - Strings only support overloading for `+` and the comparison operators. 192 | - Non-numeric, non-string primitives don't support overloading at all. 193 | - When one operand is an ordinary Object and the other is an Object with overloaded operators, the ordinary object is first coerced to some kind of primitive, making it less useful unless both operands were set up for overloading. 194 | - ToPrimitive, ToNumber, ToString, etc are *not* extended to ever return non-primitives. 195 | - Only built-in operators are supported; there are no user-defined operators. 196 | - Using overloaded operators requires the `@use: operators` statement, adding a little bit of friction, so overloaded operators are more likely to be used when they "pay for" that friction themselves from the perspective of a library user. 197 | 198 | ## Usage documentation 199 | 200 | This section includes high-level for how to use and define overloaded operators, targeted at JavaScript programmers potentially using the feature. For low-level spec-like text, see [PROTOSPEC.md](https://github.com/littledan/proposal-operator-overloading/blob/master/PROTOSPEC.md). 201 | 202 | ### Using operators 203 | 204 | With this proposal, operators can be overloaded on certain JavaScript objects that declare themselves as having overloaded operators. 205 | 206 | The following operators may have overloaded behavior: 207 | - Mathematical operators: unary `+`, `-`, `++`, `--`; binary `+`, `-`, `*`, `/`, `%`, `**` 208 | - Bitwise operators: unary `~`; binary `&`, `^`, `|`, `<<`, `>>`, `>>>` 209 | - Comparison operators: `==`, `<`, `>`, `<=`, `>=` 210 | - Possibly, integer-indexed property access: `[]`, `[]=` 211 | 212 | The definition of `>`, `<=` and `>=` is derived from `<`, and the definition of assigning operators like `+=` is derived their corresponding binary operator, for example `+`. 213 | 214 | The following operators do not support overloading: 215 | - `!`, `&&`, `||` (boolean operations--always does ToBoolean first, and then works with the boolean) 216 | - `===` and the built-in SameVale and SameValueZero operations (always uses the built-in strict equality definition) 217 | - `.` and `[]` with non-integer values (these are property access; use Proxy to overload) 218 | - `()` (calling a function--use a Proxy to overload) 219 | - `,` (just returns the right operand) 220 | - With future proposals, `|>`, `?.`, `?.[`, `?.(`, `??` (based on function calls, property access, and checks against the specific null/undefined values, so similar to the above) 221 | 222 | To use operator overloading, import a module that exports a class, and enable operators on it using a `@use: operators` declaration. 223 | 224 | ### `with operators from` declarations 225 | 226 | Operator overloading is only enabled for the classes that you specifically opt in to. To do this overloading, use a `@use: operators` declaration, follwed by a comma-separated list of classes that overload operators that you want to enable. This declaration is a form of [built-in decorator](https://github.com/littledan/proposal-built-in-decorators/). 227 | 228 | For example, if you have two classes, `Vector` and `Scalar`, which support overloaded operators, you can 229 | 230 | ```js 231 | import { Vector, Scalar } from "./module.mjs"; 232 | 233 | new Vector([1, 2, 3]) * new Scalar(3); // TypeError: operator overloading on Vector and Scalar is not enabled 234 | 235 | with operators from Vector, Scalar; 236 | 237 | new Vector([1, 2, 3]) * new Scalar(3); // Works, returning new Vector([3, 6, 9]) 238 | ```` 239 | 240 | The scope of enabling operators is based on JavaScript blocks (e.g., you can enable operators within a specific function, rather than globally). By default, built-in types like `String`, `Number` and `BigInt` already have operators enabled. 241 | 242 | ### The `Operators` factory function 243 | 244 | Recommended usage: 245 | ```js 246 | const operators = Operators(operatorDefinitions [, leftOrRightOperatorDefinitions...]) 247 | class MyClass extends operators { /* ... */ } 248 | Object.preventExtensions(MyClass); 249 | ``` 250 | 251 | The `Operators` function is called with one required argument, which is a dictionary of operator definitions. The property keys are operator names like `+` and the values are functions, which take two arguments, which implement the operator. The dictionary may also have an `open` property, as described for `@Operators.overloaded` above. 252 | 253 | The subsequent parameters of `Operators` are similar dictionaries of operator definitions, used for defining the behavior of operators when one of the parameters is of a type declared previously: they must have either a `left:` or `right:` property, indicating the type of the other operand. 254 | 255 | Note: The `Operators` function and the above decorators could be exposed from a [built-in module](https://github.com/tc39/proposal-javascript-standard-library/) rather than being a property of the global object, depending on how that proposal goes. 256 | 257 | ## Q/A 258 | 259 | ### How does this proposal compare to operator overloading in other languages? 260 | 261 | For a detailed investigation, see [LANGCOMP.md](https://github.com/littledan/proposal-operator-overloading/blob/master/LANGCOMP.md). tl;dr: 262 | - It's a pretty popular design choice to conservatively support overloading only on some operators, and to define some in terms of others, as this proposal does. User-defined operators have been difficult to varying extents in other programming languages. 263 | - The way this proposal dispatches on the two operands is somewhat novel, most similar to Matlab. Unfortunately, of the established, popular mechanisms meet the design goals articulated in this document. 264 | 265 | ### Can this work with subclasses, rather than only defining overloading on base classes? 266 | 267 | That would be equivalent to giving overloading behavior to existing objects. For example, imagine `SubclassOperators` as a sort of mixin for `Operators`, taking the superclass as its first argument, and then added operator overloading behavior to the return value of the superclass's constructor. Then, the following code would add operator overloading behavior to an unsuspecting object if we permitted operator overloading to be triggered by a decorator on a class that inherited from any other class! 268 | 269 | ```js 270 | function addOverloads(obj) { 271 | class SuperClass { constructor() { return obj; } } 272 | class SubClass extends SubclassOperators(SuperClass, {"+"(a, b) { /* ... */ }}) {} 273 | new SubClass(obj); 274 | return obj; 275 | } 276 | ``` 277 | The reason that this would modify the existing instance is that `SubClass` would put operator overloading behavior on whatever is returned from the super constructor, and that super constructor returns the existing object! Even if you don't use `with operators from`, there is suddenly different behavior when using operators on the object (throwing exceptions). 278 | 279 | Let's avoid this level of dynamic-ness, and make the language more predictable by keeping it a static, unchange-able property of an object whether it overloads operators or not. 280 | 281 | ### Can't we allow monkey-patching, for mocking, etc? 282 | 283 | You can do mocking by creating a separate operator-overloaded class which works like the one you're trying to mock, or even interacts with it. Or, you can make your own hooks into the operator definitions to allow mocking. But letting any code reach into the definition of the operators for any other type risks making operators much less reliable than JavaScript programmers are accustomed to. 284 | 285 | ### Why does this have to be based on classes? I don't like classes! 286 | 287 | It doesn't *have* to be based on classes, but the reason the above examples use inheritance is that a base class constructor gives a chance to return a truly unique object, with an internal slot that guides the overloading behavior. It's not clear how to get that out of object literals, but you can use the above API in a way like this, if you'd like: 288 | 289 | ```js 290 | function makePoint(obj) { return Object.assign(new pointOps, obj); } 291 | const pointOps = Operators({ "+"(a, b) { return makePoint({x: a.x + b.x, y: a.y + b.y}); }); 292 | let point = makePoint({ x: 1, y: 2 }); 293 | with operators from pointOps; 294 | (point + point).y; // 4 295 | ``` 296 | 297 | In the future, value types and/or typed objects could give a more ergonomic syntax which might not involve classes. 298 | 299 | ### Why not use symbols instead of a whole new dispatch mechanism? 300 | 301 | Symbols would allow monkey-patching and a general lack of robustness. They don't give a clear way to dispatch on the right operand, without requiring a *second* property access (like Python). The Python-style dispatch also has a left-to-right bias, which is unfortunate. Symbols also don't let us avoid doing additional property accesses on existing objects, the way that this proposal does enable us to do. 302 | 303 | ### Why doesn't this let me define my own operator token? 304 | 305 | This proposal only allows overloading built-in operators, because: 306 | 307 | - There's significant concern about "punctuation overload" in JavaScript programs, already growing more fragile with private fields/methods (`#`) and decorators (`@`). Too many kinds of punctuation could make programs hard to read. 308 | - User-defined precedence for such tokens is unworkable to parse. 309 | - Hopefully the [pipeline operator](https://github.com/tc39/proposal-pipeline-operator) and [optional chaining](https://github.com/tc39/proposal-optional-chaining) will solve many of the cases that would motivate these operators. 310 | - We deliberately want to limit the syntactic divergence of JavaScript programs. 311 | 312 | User-defined operator tokens may be a worthwhile proposal, but I (littledan) would be somewhat uncomfortable championing them for the above reasons. 313 | 314 | ### Why doesn't this proposal allow ordinary functions to be called in infix contexts? 315 | 316 | For example, Haskell permits this capability, using backticks. 317 | 318 | Such a capability could be useful, but it incurs the issues with adding more punctuation (see the previous Q/A entry), while not providing as terse results as overloading built-in operators. Method chaining or the pipeline operator can be used in many of the cases where infix function application could also be used. 319 | 320 | ### Should operator overloading use inheritance-based multiple dispatch involving the prototype chain? 321 | 322 | This proposal has opted against using something like Slate's Prototype Multiple Dispatch, because: 323 | 324 | - This is really complicated to implement and optimize reliably. 325 | - It's not clear what important use cases there are that aren't solved by single-level dispatch. 326 | 327 | ### If you define your other-type overloads based on a previously defined type, how do you know which type came first? 328 | 329 | If you have operators defined in two different modules, then to define overloads between them, import one module from the other, and don't make a circularity between the two. If you do this, the loading order wil lbe determinstic. The one that imports the other one is responsible for defining the overloads between the two types. 330 | 331 | ### How does operator overloading relate to other proposals? 332 | 333 | #### Decorators 334 | 335 | [Decorators](https://github.com/tc39/proposal-decorators/) (Stage 2) could be used for a more ergonomic way to define operator overloading, as described in a previous version of this README. However, the decorators proposal is not yet stable, with changes still being discussed, so it's a bit early to propose a concrete decorator syntax for operator overloading. 336 | 337 | #### BigInt and BigDecimal 338 | 339 | [BigInt](https://github.com/tc39/proposal-bigint/) (Stage 4) provides definitions for how operators work on just one new type, representing arbitrary-precision integers. [BigDecimal](https://github.com/littledan/proposal-bigdecimal) (Stage 0) represents another, for arbitrary-precision decimals. This proposal generalizes that to types defined in JavaScript. 340 | 341 | #### Records and Tuples 342 | 343 | [Records and Tuples](https://github.com/tc39/proposal-record-tuple) (Stage 1) provides a deeply immutable compound primitive notion, superficially analogous to Objects and Arrays, with value semantics. 344 | 345 | If either operator overloading and records and tuples advances past Stage 1, then a next step for the other proposal would be to define semantics for the two features to be used together. One possibility is that for the return value of `Operators` to have a method that would take a Record or Tuple and return a new one with operators overloaded (details TBD). 346 | 347 | #### Typed Objects and Value Types 348 | 349 | [Typed Objects](https://github.com/tschneidereit/proposal-typed-objects/blob/master/explainer.md) is a proposal for efficient, fixed-shape objects. 350 | 351 | Value Types is an idea in TC39 about user-definable primitive types. At some points in the past, it was proposed that operator overloading be tied to value types. 352 | 353 | Neither proposal is currently championed in TC39, but Records and Tuples presents a sort of first step towards Value Types. 354 | 355 | When these proposals mature more, it will be good to look into how operator overloading can be enabled for Typed Objects and Value Types. The idea in this repository is to not limit operator overloading to those two types of values, but to also permit operator overloading for ordinary objects. 356 | 357 | #### Extended numeric literals 358 | 359 | The [extended numeric literals](https://github.com/tc39/proposal-extended-numeric-literals) proposal (Stage 1) allows numeric-like types such as `3@px` or `5.2@m` to be defined and used ergonomically. Extended numeric literals and operator overloading could fit well together, but they don't depend on each other and can each be used separately. This README omits use of extended numeric literals, for simplicity and to focus on the operator overloading aspects. 360 | 361 | ### How does operator overloading interact with Proxy and membrane systems? 362 | 363 | In this proposal, operators remain *not* operations visible in the meta-object protocol. Objects with overloaded operators don't even undergo the typical object coercion. However, this proposal still attempts to mesh well with membrane systems. 364 | 365 | All operator-overloaded values are objects, so any technique that's used to create or access them can be mediated by membrane wrapping. The value returned from the membrane can be overloaded in a separate, membrane-mediated way, assuming collaboration between the overloaded object and the membrane system (otherwise there's no introspection API to see which operators to overload). 366 | 367 | A membrane system which runs early in the program's execution (like the freeze-the-world systems) can monkey-patch and replace the `Operators` object to provide this collaboration; therefore, there is no need for any particular additional hooks. At a minimum, even without replacing the `Operators` object, the membrane can deny use of overloaded operators for the object on the other side of the membrane. 368 | 369 | `with operator from` declarations provide a further defense: Those declarations prove that the piece of the program has access to (a piece of) the class defining overloaded operators. This works because the lookup of the internal slot `[[OperatorSetDefinition]]` does is not transparent to Proxies. A membrane system can deny access to that original operator set, and instead replace it with a separate class which overloads operators in a membrane-mediated way. In this way, even if an overloaded value "leaks", the right to call its operators is controlled by the class, which forms a capability object. 370 | 371 | ### Could this proposal allow overloading `[]`? 372 | 373 | Possibly. `[]` is property access, not an operator as such. ES6 introduced Proxy, which lets developers define custom semantics for property access. Unfortunately, this capability has certain issues: 374 | - This proposal is based on `Operators` factories, which produce constructors that return new objects with operator overloading. `Proxy` also returns new object instances. It's not possible to use both Proxy and the mechanism in this repository together, since operator overloading doesn't forward through Proxy traps. Therefore, some other mechanism is needed. 375 | - Proxy has so far not yet been optimized in JavaScript engines as much as some might hope. It's not clear exactly what the cause is, but one factor may be how general Proxy capabilities are. The proposal for overloading `[]` is much more restricted in its power, potentially making it more easily optimizable. 376 | - From the perspective of many JavaScript developers and even library authors, at a high level, it's an "implementation detail" that array index access is based on JavaScript property access; for them, overloading this way is consistent with their mental model. 377 | 378 | The overloading of `[]` proposed here would be based on the semantics of Integer Indexed Exotic Objects. It wouldn't add or change anything about JavaScript's meta-object protocol, and it would only change the semantics of objects with overloading of `[]` declared. 379 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | This directory contains a rough implementation of an [operator overloading proposal](https://github.com/littledan/proposal-operator-overloading/). It is divided into two npm packages, which are in subdirectories: 2 | - transform/ contains the source of @littledan/plugin-transform-operator-overloading, which 3 | - shim/ contains the source of @littledan/operator-overloading-shim, runtime support for the Babel plugin 4 | 5 | Find instructions on usage in transform/README.md. 6 | 7 | -------------------------------------------------------------------------------- /src/shim/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /src/shim/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "google", 3 | "parserOptions": { 4 | "ecmaVersion": 2018, 5 | "sourceType": "module" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/shim/README.md: -------------------------------------------------------------------------------- 1 | This package contains runtime support for an [operator overloading proposal](https://github.com/littledan/proposal-operator-overloading/) for JavaScript, to be used in the Babel plugin `@littledan/plugin-transform-operator-overloading`. 2 | -------------------------------------------------------------------------------- /src/shim/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@littledan/operator-overloading-shim", 3 | "version": "0.0.4", 4 | "description": "Shim for runtime support for an operator overloading proposal", 5 | "main": "build/shim.js", 6 | "scripts": { 7 | "test": "jasmine shim.spec.js", 8 | "build": "babel shim.js -d build" 9 | }, 10 | "repository": "https://github.com/littledan/proposal-operator-overloading/tree/master/src/shim", 11 | "keywords": [ 12 | "operator-overloading", 13 | "tc39" 14 | ], 15 | "author": "Daniel Ehrenberg", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/littledan/proposal-operator-overloading/issues" 19 | }, 20 | "homepage": "https://github.com/littledan/proposal-operator-overloading/blob/master/src/shim/README.md", 21 | "devDependencies": { 22 | "@babel/cli": "^7.2.3", 23 | "@babel/core": "^7.2.2", 24 | "@babel/preset-env": "^7.2.2", 25 | "acorn": "^6.0.4", 26 | "eslint": "^5.10.0", 27 | "eslint-config-google": "^0.11.0", 28 | "jasmine": "^3.3.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/shim/shim.js: -------------------------------------------------------------------------------- 1 | // Runtime support for operator overloading 2 | 3 | // This module exports the Operators object and 4 | // a number of _-prefixed functions which a Babel 5 | // transform can call out to. 6 | 7 | // This code doesn't attempt to be 100% spec-compliant, 8 | // high-performance, or monkey-patch proof, but just to 9 | // get the basic cases right for prototyping. 10 | 11 | const OperatorSet = Symbol('OperatorSet'); 12 | const OperatorDefinition = Symbol('OperatorDefinition'); 13 | 14 | const binaryOperators = ['-', '*', '/', '%', '**', '&', '^', '|', '<<', '>>', '>>>', '==', '+', '<']; 15 | const binaryOperatorSet = new Set(binaryOperators); 16 | const unaryOperators = ['pos', 'neg', '++', '--', '~']; 17 | const unaryOperatorSet = new Set(unaryOperators); 18 | const allOperators = binaryOperators.concat(unaryOperators, ['[]', '[]=']); 19 | const operatorSet = new Set(allOperators); 20 | 21 | // To implement operators on built-in types, back them by 22 | // how JavaScript already works. 23 | // No harm done including additional operators! 24 | const identityOperators = { 25 | '-'(a, b) { 26 | return a - b; 27 | }, 28 | '*'(a, b) { 29 | return a * b; 30 | }, 31 | '/'(a, b) { 32 | return a / b; 33 | }, 34 | '%'(a, b) { 35 | return a % b; 36 | }, 37 | '**'(a, b) { 38 | return a ** b; 39 | }, 40 | '&'(a, b) { 41 | return a & b; 42 | }, 43 | '^'(a, b) { 44 | return a ^ b; 45 | }, 46 | '|'(a, b) { 47 | return a | b; 48 | }, 49 | '<<'(a, b) { 50 | return a << b; 51 | }, 52 | '>>'(a, b) { 53 | return a >> b; 54 | }, 55 | '>>>'(a, b) { 56 | return a >>> b; 57 | }, 58 | '=='(a, b) { 59 | return a == b; 60 | }, 61 | '+'(a, b) { 62 | return a + b; 63 | }, 64 | '<'(a, b) { 65 | return a < b; 66 | }, 67 | 'pos'(a) { 68 | return +a; 69 | }, 70 | 'neg'(a) { 71 | return -a; 72 | }, 73 | '++'(a) { 74 | return ++a; 75 | }, 76 | '--'(a) { 77 | return --a; 78 | }, 79 | '~'(a) { 80 | return ~a; 81 | }, 82 | }; 83 | 84 | 85 | Number[OperatorDefinition] = 86 | Number.prototype[OperatorSet] = { 87 | OperatorCounter: 0, 88 | SelfOperatorDefinition: identityOperators, 89 | OpenOperators: binaryOperatorSet, 90 | }; 91 | 92 | if (typeof BigInt !== 'undefined') { 93 | BigInt[OperatorDefinition] = 94 | BigInt.prototype[OperatorSet] = { 95 | OperatorCounter: 1, 96 | SelfOperatorDefinition: identityOperators, 97 | LeftOperatorDefinitions: [identityOperators], 98 | RightOperatorDefinitions: [identityOperators], 99 | OpenOperators: binaryOperatorSet, 100 | }; 101 | } 102 | 103 | String[OperatorDefinition] = 104 | String.prototype[OperatorSet] = { 105 | OperatorCounter: 2, 106 | SelfOperatorDefinition: identityOperators, 107 | LeftOperatorDefinitions: [identityOperators, identityOperators], 108 | RightOperatorDefinitions: [identityOperators, identityOperators], 109 | OpenOperators: ['+', '==', '<'], 110 | }; 111 | 112 | let OperatorCounter = 3; 113 | 114 | function cleanTable(table, operatorList) { 115 | const outTable = {}; 116 | for (const operator of operatorList) { 117 | const fn = table[operator]; 118 | if (typeof fn !== 'undefined') { 119 | if (typeof fn !== 'function') { 120 | throw new TypeError('Operators must be functions'); 121 | } 122 | outTable[operator] = fn; 123 | } 124 | } 125 | return outTable; 126 | } 127 | 128 | function partitionTables(tables) { 129 | const left = []; 130 | const right = []; 131 | for (let table of tables) { 132 | const leftType = table.left; 133 | const rightType = table.right; 134 | table = cleanTable(table, binaryOperators); 135 | if (typeof leftType !== 'undefined') { 136 | if (typeof rightType !== 'undefined') { 137 | throw new TypeError('overload table must not be both left and right'); 138 | } 139 | const leftSet = leftType[OperatorDefinition]; 140 | if (typeof leftSet === 'undefined') { 141 | throw new TypeError( 142 | 'the left: value must be a class with operators overloaded'); 143 | } 144 | for (const key of Object.keys(table)) { 145 | if (!leftSet.OpenOperators.has(key)) { 146 | throw new TypeError( 147 | `the operator ${key} may not be overloaded on the provided type`); 148 | } 149 | } 150 | // "Backwards" because this new operator type is on the right 151 | // and the other argument is on the left 152 | right[leftSet.OperatorCounter] = table; 153 | } else { 154 | if (typeof rightType === 'undefined') { 155 | throw new TypeError('Either left: or right: must be provided'); 156 | } 157 | const rightSet = rightType[OperatorDefinition]; 158 | if (typeof rightSet === 'undefined') { 159 | throw new TypeError( 160 | 'the right: value must be a class with operators overloaded'); 161 | } 162 | for (const key of Object.keys(table)) { 163 | if (!rightSet.OpenOperators.has(key)) { 164 | throw new TypeError( 165 | `the operator ${key} may not be overloaded on the provided type`); 166 | } 167 | } 168 | left[rightSet.OperatorCounter] = table; 169 | } 170 | } 171 | return {left, right}; 172 | } 173 | 174 | function makeOpenSet(open) { 175 | if (typeof open !== 'undefined') { 176 | open = [...open]; 177 | for (const operator of open) { 178 | if (!operatorSet.has(operator)) { 179 | throw new TypeError(`Unrecognized operator ${operator}`); 180 | } 181 | } 182 | } 183 | return new Set(open); 184 | } 185 | 186 | function CanonicalNumericIndexString(key) { 187 | if (typeof key !== 'string') return undefined; 188 | if (key === '-0') return -0; 189 | const n = Number(key); 190 | if (String(n) !== key) return undefined; 191 | return n; 192 | } 193 | 194 | function IsInteger(n) { 195 | if (typeof n !== 'number') return false; 196 | if (Object.is(n, NaN) || n === Infinity || n === -Infinity) return false; 197 | return Math.floor(Math.abs(n)) === Math.abs(n); 198 | } 199 | 200 | function IsBadIndex(n) { 201 | return !IsInteger(n) || n < 0 || Object.is(n, -0); 202 | } 203 | 204 | export function Operators(table, ...tables) { 205 | const counter = OperatorCounter++; 206 | 207 | let open = table.open; 208 | table = cleanTable(table, allOperators); 209 | const {left, right} = partitionTables(tables); 210 | open = makeOpenSet(open); 211 | 212 | const set = { 213 | OperatorCounter: counter, 214 | SelfOperatorDefinition: table, 215 | LeftOperatorDefinitions: left, 216 | RightOperatorDefinitions: right, 217 | OpenOperators: open, 218 | }; 219 | 220 | let Overloaded; 221 | if ('[]' in table || '[]=' in table) { 222 | Overloaded = class { 223 | constructor() { 224 | // Unfortunately, we have to close over proxy to invoke Get("length"), 225 | // so that the receiver will be accurate (e.g., in case it uses private) 226 | const proxy = new Proxy({__proto__: new.target.prototype, [OperatorSet]: set}, { 227 | getOwnPropertyDescriptor(target, key) { 228 | const n = CanonicalNumericIndexString(key); 229 | if (n === undefined) return Reflect.getOwnPropertyDescriptor(target, key, proxy); 230 | if (IsBadIndex(n)) return undefined; 231 | const length = Number(proxy.length); 232 | if (n >= length) return undefined; 233 | const value = table['[]'](proxy, n); 234 | return {value, writable: true, enumerable: true, configurable: false}; 235 | }, 236 | has(target, key) { 237 | const n = CanonicalNumericIndexString(key); 238 | if (n === undefined) return Reflect.has(target, key, proxy); 239 | if (IsBadIndex(n)) return false; 240 | const length = Number(proxy.length); 241 | return n < length; 242 | }, 243 | defineProperty(target, key, desc) { 244 | const n = CanonicalNumericIndexString(key); 245 | if (n === undefined) return Reflect.defineProperty(target, key, desc, proxy); 246 | if (IsBadIndex(n)) return false; 247 | if (desc.writable === false || 248 | desc.enumerable === false || 249 | desc.configurable === true) return false; 250 | table['[]='](proxy, n, desc.value); 251 | Reflect.defineProperty(target, key, desc, proxy); // Necessary for integrity checks 252 | return true; 253 | }, 254 | get(target, key) { 255 | const n = CanonicalNumericIndexString(key); 256 | if (n === undefined) return Reflect.get(target, key, proxy); 257 | if (IsBadIndex(n)) return undefined; 258 | const length = Number(proxy.length); 259 | if (n >= length) return undefined; 260 | const value = table['[]'](proxy, n); 261 | return value; 262 | }, 263 | set(target, key, value) { 264 | const n = CanonicalNumericIndexString(key); 265 | if (n === undefined) return Reflect.set(target, key, value, proxy); 266 | if (IsBadIndex(n)) return false; 267 | table['[]='](proxy, n, value); 268 | return true; 269 | }, 270 | ownKeys(target) { 271 | const length = Number(proxy.length); 272 | let keys = []; 273 | for (let i = 0; i < length; i++) keys.push(String(i)); 274 | keys = keys.concat(Reflect.ownKeys(target, proxy)); 275 | return keys; 276 | }, 277 | }); 278 | return proxy; 279 | } 280 | }; 281 | } else { 282 | Overloaded = class { 283 | constructor() { 284 | this[OperatorSet] = set; 285 | } 286 | }; 287 | } 288 | Overloaded[OperatorDefinition] = set; 289 | 290 | return Overloaded; 291 | } 292 | 293 | // klass => Array of {operator, definition, options} 294 | const decoratorOperators = new WeakMap(); 295 | 296 | function OperatorsOverloaded(descriptor, open) { 297 | // This algorithm doesn't contain enough validation 298 | // (of options and open) and is too inefficient 299 | descriptor.finisher = (klass) => { 300 | const args = [{...open}]; 301 | const operators = decoratorOperators.get(klass); 302 | if (operators === undefined) throw new TypeError('No operators overloaded'); 303 | decoratorOperators.delete(klass); 304 | // Gratuitiously inefficient algorithm follows 305 | for (const {operator, definition, options} of operators) { 306 | if (options === undefined) { 307 | args[0][operator] = definition; 308 | } else { 309 | let obj = args.find((entry) => 310 | entry.right === options.right || entry.left === options.left); 311 | if (!obj) { 312 | obj = {...options}; 313 | args.push(obj); 314 | } 315 | obj[operator] = definition; 316 | } 317 | } 318 | // get operators and process them into args 319 | const superclass = Operators(...args); 320 | Object.setPrototypeOf(klass, superclass); 321 | Object.setPrototypeOf(klass.prototype, superclass.prototype); 322 | }; 323 | } 324 | 325 | Operators.overloaded = function(arg) { 326 | if (arg[Symbol.toStringTag] === 'Descriptor') { 327 | return OperatorsOverloaded(arg); 328 | } else { 329 | return (descriptor) => OperatorsOverloaded(descriptor, arg); 330 | } 331 | }; 332 | 333 | Operators.define = function(operator, options) { 334 | return function(descriptor) { 335 | if (descriptor.kind !== 'method') { 336 | throw new TypeError('@Operator.define must be used on a method'); 337 | } 338 | const definition = descriptor.descriptor.value; 339 | descriptor.finisher = (klass) => { 340 | let operators = decoratorOperators.get(klass); 341 | if (operators === undefined) { 342 | operators = []; 343 | decoratorOperators.set(klass, operators); 344 | } 345 | operators.push({operator, definition, options}); 346 | }; 347 | }; 348 | }; 349 | 350 | const defaultOperators = [0, 1, 2]; 351 | export function _declareOperators(parent = defaultOperators) { 352 | return new Set([...parent]); 353 | } 354 | 355 | export function _withOperatorsFrom(set, ...additions) { 356 | for (const klass of additions) { 357 | const definition = klass[OperatorDefinition]; 358 | if (!definition) { 359 | throw new TypeError( 360 | 'with operator from must be invoked with a class ' + 361 | 'with overloaded operators'); 362 | } 363 | set.add(definition.OperatorCounter); 364 | } 365 | } 366 | 367 | function isNumeric(x) { 368 | return typeof x === 'number' || typeof x === 'bigint'; 369 | } 370 | 371 | function isObject(x) { 372 | return typeof x === 'object' && x !== null || typeof x === 'function'; 373 | } 374 | 375 | function hasOverloadedOperators(obj) { 376 | return isObject(obj) && OperatorSet in obj; 377 | } 378 | 379 | function ToNumericOperand(a) { 380 | if (isNumeric(a)) return a; 381 | if (hasOverloadedOperators(a)) return a; 382 | return +a; // Sloppy on BigInt wrappers 383 | } 384 | 385 | function checkPermitted(a, operatorSet, operator) { 386 | const operatorCounter = a[OperatorSet].OperatorCounter; 387 | if (!operatorSet.has(operatorCounter)) { 388 | throw new TypeError( 389 | '`with operators from` declaration missing before overload usage' + 390 | ` in evaluating ${operator}`); 391 | } 392 | } 393 | 394 | function assertFunction(fn, operator) { 395 | if (typeof fn !== 'function') { 396 | throw new TypeError(`No overload found for ${operator}`); 397 | } 398 | } 399 | 400 | function dispatchBinaryOperator(operator, a, b, operatorSet) { 401 | checkPermitted(a, operatorSet, operator); 402 | if (a[OperatorSet] === b[OperatorSet]) { 403 | const fn = a[OperatorSet].SelfOperatorDefinition[operator]; 404 | assertFunction(fn, operator); 405 | return fn(a, b); 406 | } else { 407 | checkPermitted(b, operatorSet, operator); 408 | let definitions; 409 | if (a[OperatorSet].OperatorCounter < b[OperatorSet].OperatorCounter) { 410 | definitions = b[OperatorSet].RightOperatorDefinitions[ 411 | a[OperatorSet].OperatorCounter]; 412 | } else { 413 | definitions = a[OperatorSet].LeftOperatorDefinitions[ 414 | b[OperatorSet].OperatorCounter]; 415 | } 416 | if (typeof definitions !== 'object') { 417 | throw new TypeError(`No overload found for ${operator}`); 418 | } 419 | const fn = definitions[operator]; 420 | assertFunction(fn, operator); 421 | return fn(a, b); 422 | } 423 | } 424 | 425 | export function _binary(operator, a, b, operatorSet) { 426 | switch (operator) { 427 | case "+": 428 | return _additionOperator(a, b, operatorSet); 429 | case "==": 430 | return _abstractEqualityComparison(a, b, operatorSet); 431 | case "!=": 432 | return !_abstractEqualityComparison(a, b, operatorSet); 433 | case "<": 434 | case ">": 435 | case "<=": 436 | case ">=": 437 | return _abstractRelationalComparison(operator, a, b, operatorSet); 438 | default: 439 | return _numericBinaryOperate(operator, a, b, operatorSet); 440 | } 441 | } 442 | 443 | // Binary -, *, /, %, **, &, ^, |, <<, >>, >>> 444 | function _numericBinaryOperate(operator, a, b, operatorSet) { 445 | if (isNumeric(a) && isNumeric(b)) return identityOperators[operator](a, b); // micro-optimization 446 | a = ToNumericOperand(a); 447 | b = ToNumericOperand(b); 448 | return dispatchBinaryOperator(operator, a, b, operatorSet); 449 | } 450 | 451 | // pos, neg, ++, --, ~ 452 | export function _unary(operator, a, operatorSet) { 453 | if (isNumeric(a)) return identityOperators[operator](a); // micro-optimization 454 | a = ToNumericOperand(a); 455 | 456 | checkPermitted(a, operatorSet, operator); 457 | const fn = a[OperatorSet].SelfOperatorDefinition[operator]; 458 | assertFunction(fn, operator); 459 | return fn(a); 460 | } 461 | 462 | function ToPrimitive(x) { 463 | // This does Number hint/default (we're just skipping @@toPrimitive) 464 | if (isObject(x)) { 465 | for (const method of ['valueOf', 'toString']) { 466 | const fn = x[method]; 467 | if (typeof fn === 'function') { 468 | const result = fn(x); 469 | if (!isObject(result)) { 470 | return result; 471 | } 472 | } 473 | } 474 | throw new TypeError('ToPrimitive failed'); // weird! 475 | } else { 476 | return x; 477 | } 478 | } 479 | 480 | function ToOperand(x) { 481 | if (hasOverloadedOperators(x)) return x; 482 | return ToPrimitive(x); 483 | } 484 | 485 | // == 486 | function _abstractEqualityComparison(x, y, operatorSet) { 487 | if (typeof x === typeof y && !isObject(x)) return x === y; 488 | if (x === null && y === void 0) return true; 489 | if (x === void 0 && y === null) return true; 490 | if (typeof x === 'boolean') { 491 | return _abstractEqualityComparison(Number(x), y, operatorSet); 492 | } 493 | if (typeof y === 'boolean') { 494 | return _abstractEqualityComparison(x, Number(y), operatorSet); 495 | } 496 | x = ToOperand(x); 497 | y = ToOperand(y); 498 | if (!hasOverloadedOperators(x) && !hasOverloadedOperators(y)) return x == y; 499 | return dispatchBinaryOperator('==', x, y, operatorSet); 500 | } 501 | 502 | // + 503 | function _additionOperator(a, b, operatorSet) { 504 | // Sloppy about String wrappers 505 | a = ToOperand(a); 506 | b = ToOperand(b); 507 | if (typeof a === 'string' || typeof b === 'string') { 508 | return a + b; 509 | } 510 | return dispatchBinaryOperator('+', a, b, operatorSet); 511 | } 512 | 513 | // <, >, <=, >= 514 | function _abstractRelationalComparison(operator, a, b, operatorSet) { 515 | a = ToOperand(a); 516 | b = ToOperand(b); 517 | let swap; let not; 518 | switch (operator) { 519 | case '<': 520 | swap = false; 521 | not = false; 522 | break; 523 | case '>': 524 | swap = true; 525 | not = false; 526 | break; 527 | case '<=': 528 | swap = true; 529 | not = true; 530 | break; 531 | case '>=': 532 | swap = false; 533 | not = true; 534 | break; 535 | default: throw new TypeError; 536 | } 537 | if (swap) { 538 | [a, b] = [b, a]; 539 | } 540 | let result; 541 | if (!hasOverloadedOperators(a) && !hasOverloadedOperators(b)) { 542 | result = a < b; 543 | } else { 544 | result = dispatchBinaryOperator('<', a, b, operatorSet); 545 | } 546 | if (not) { 547 | result = !result; 548 | } 549 | return result; 550 | } 551 | -------------------------------------------------------------------------------- /src/shim/shim.spec.js: -------------------------------------------------------------------------------- 1 | const shim = require("./build/shim.js"); 2 | 3 | describe("Operators without overloading registered", () => { 4 | const operators = shim._declareOperators(); 5 | it('addition on Numbers works as usual', () => { 6 | expect(shim._binary("+", 1, 2, operators)).toBe(3); 7 | }); 8 | it('addition on BigInt works as usual', () => { 9 | expect(shim._binary("+", 1n, 2n, operators)).toBe(3n); 10 | }); 11 | it('addition on Strings works as usual', () => { 12 | expect(shim._binary("+", "ab", "cd", operators)).toBe("abcd"); 13 | }); 14 | it('addition between String and Number', () => { 15 | expect(shim._binary("+", "ab", 1, operators)).toBe("ab1"); 16 | expect(shim._binary("+", 1, "ab", operators)).toBe("1ab"); 17 | }); 18 | it('addition between String and BigInt', () => { 19 | expect(shim._binary("+", "ab", 1n, operators)).toBe("ab1"); 20 | expect(shim._binary("+", 1n, "ab", operators)).toBe("1ab"); 21 | }); 22 | it('== works as usual', () => { 23 | expect(shim._binary("==", 1, 2, operators)).toBe(false); 24 | expect(shim._binary("==", 1, 2n, operators)).toBe(false); 25 | expect(shim._binary("==", 1, 1, operators)).toBe(true); 26 | expect(shim._binary("==", 1, 1n, operators)).toBe(true); 27 | expect(shim._binary("==", 1, true, operators)).toBe(true); 28 | expect(shim._binary("==", 1, false, operators)).toBe(false); 29 | expect(shim._binary("==", 0, false, operators)).toBe(true); 30 | expect(shim._binary("==", 1, "1", operators)).toBe(true); 31 | expect(shim._binary("==", "1", "1", operators)).toBe(true); 32 | expect(shim._binary("==", "1", "2", operators)).toBe(false); 33 | expect(shim._binary("==", "1", true, operators)).toBe(true); 34 | expect(shim._binary("==", null, undefined, operators)).toBe(true); 35 | expect(shim._binary("==", undefined, null, operators)).toBe(true); 36 | expect(shim._binary("==", undefined, 0, operators)).toBe(false); 37 | expect(shim._binary("==", undefined, NaN, operators)).toBe(false); 38 | expect(shim._binary("==", null, NaN, operators)).toBe(false); 39 | expect(shim._binary("==", null, 0, operators)).toBe(false); 40 | }); 41 | it('== works with object wrappers', () => { 42 | expect(shim._binary("==", Object(1), 2, operators)).toBe(false); 43 | expect(shim._binary("==", Object(1), 2n, operators)).toBe(false); 44 | expect(shim._binary("==", Object(1), 1, operators)).toBe(true); 45 | expect(shim._binary("==", Object(1), 1n, operators)).toBe(true); 46 | expect(shim._binary("==", Object(1), true, operators)).toBe(true); 47 | expect(shim._binary("==", Object(1), false, operators)).toBe(false); 48 | expect(shim._binary("==", Object(0), false, operators)).toBe(true); 49 | expect(shim._binary("==", Object(1), "1", operators)).toBe(true); 50 | expect(shim._binary("==", Object("1"), "1", operators)).toBe(true); 51 | expect(shim._binary("==", Object("1"), "2", operators)).toBe(false); 52 | expect(shim._binary("==", Object("1"), true, operators)).toBe(true); 53 | }); 54 | it('< works on primitives', () => { 55 | expect(shim._binary('<', 1, 2, operators)).toBe(true); 56 | expect(shim._binary('<', 2, 2, operators)).toBe(false); 57 | expect(shim._binary('<', 3, 2, operators)).toBe(false); 58 | expect(shim._binary('<', 1n, 2, operators)).toBe(true); 59 | expect(shim._binary('<', 2n, 2, operators)).toBe(false); 60 | expect(shim._binary('<', 3n, 2n, operators)).toBe(false); 61 | expect(shim._binary('<', 1n, 2n, operators)).toBe(true); 62 | expect(shim._binary('<', 2n, 2n, operators)).toBe(false); 63 | expect(shim._binary('<', 3n, 2, operators)).toBe(false); 64 | expect(shim._binary('<', "1", 2, operators)).toBe(true); 65 | expect(shim._binary('<', "2", 2, operators)).toBe(false); 66 | expect(shim._binary('<', "3", 2, operators)).toBe(false); 67 | expect(shim._binary('<', "100", 11, operators)).toBe(false); 68 | expect(shim._binary('<', "100", "11", operators)).toBe(true); 69 | }); 70 | it('< works on with object wrappers', () => { 71 | expect(shim._binary('<', Object(1), 2, operators)).toBe(true); 72 | expect(shim._binary('<', Object(2), 2, operators)).toBe(false); 73 | expect(shim._binary('<', Object(3), 2, operators)).toBe(false); 74 | expect(shim._binary('<', Object(1n), 2, operators)).toBe(true); 75 | expect(shim._binary('<', Object(2n), 2, operators)).toBe(false); 76 | expect(shim._binary('<', Object(3n), 2n, operators)).toBe(false); 77 | expect(shim._binary('<', Object(1n), 2n, operators)).toBe(true); 78 | expect(shim._binary('<', Object(2n), 2n, operators)).toBe(false); 79 | expect(shim._binary('<', Object(3n), 2, operators)).toBe(false); 80 | expect(shim._binary('<', Object("1"), 2, operators)).toBe(true); 81 | expect(shim._binary('<', Object("2"), 2, operators)).toBe(false); 82 | expect(shim._binary('<', Object("3"), 2, operators)).toBe(false); 83 | expect(shim._binary('<', Object("100"), 11, operators)).toBe(false); 84 | expect(shim._binary('<', Object("100"), "11", operators)).toBe(true); 85 | }); 86 | it('> >= <= also work', () => { 87 | expect(shim._binary('>', 1, 2, operators)).toBe(false); 88 | expect(shim._binary('>', 2, 2, operators)).toBe(false); 89 | expect(shim._binary('>', 3, 2, operators)).toBe(true); 90 | expect(shim._binary('<=', 1, 2, operators)).toBe(true); 91 | expect(shim._binary('<=', 2, 2, operators)).toBe(true); 92 | expect(shim._binary('<=', 3, 2, operators)).toBe(false); 93 | expect(shim._binary('>=', 1, 2, operators)).toBe(false); 94 | expect(shim._binary('>=', 2, 2, operators)).toBe(true); 95 | expect(shim._binary('>=', 3, 2, operators)).toBe(true); 96 | }); 97 | it('* works', () => { 98 | expect(shim._binary('*', 2, 3, operators)).toBe(6); 99 | expect(shim._binary('*', "2", 3, operators)).toBe(6); 100 | expect(shim._binary('*', "2", "3", operators)).toBe(6); 101 | expect(shim._binary('*', 2n, 3n, operators)).toBe(6n); 102 | expect(shim._binary('*', Object(2), 3, operators)).toBe(6); 103 | expect(shim._binary('*', Object("2"), 3, operators)).toBe(6); 104 | expect(shim._binary('*', Object("2"), "3", operators)).toBe(6); 105 | expect(shim._binary('*', Object(2n), 3n, operators)).toBe(6n); 106 | }); 107 | it('++ works', () => { 108 | expect(shim._unary('++', 2, operators)).toBe(3); 109 | expect(shim._unary('++', "2", operators)).toBe(3); 110 | expect(shim._unary('++', 2n, operators)).toBe(3n); 111 | expect(shim._unary('++', Object(2), operators)).toBe(3); 112 | expect(shim._unary('++', Object("2"), operators)).toBe(3); 113 | expect(shim._unary('++', Object(2n), operators)).toBe(3n); 114 | }); 115 | }); 116 | 117 | describe('simple overloading', () => { 118 | 119 | const Ops = shim.Operators({ 120 | '+'(a, b) { 121 | return new Vector(a.contents.map((elt, i) => elt + b.contents[i])); 122 | } 123 | }); 124 | 125 | class Vector extends Ops { 126 | constructor(contents) { super(); this.contents = contents; } 127 | } 128 | 129 | const vec = new Vector([1, 2, 3]); 130 | 131 | it('+ throws when not in operator set', () => { 132 | const operators = shim._declareOperators(); 133 | expect(() => shim._binary("+", vec, vec, operators)).toThrowError(TypeError); 134 | }); 135 | 136 | it('+ is permitted among vectors, banned in interoperation', () => { 137 | const operators = shim._declareOperators(); 138 | shim._withOperatorsFrom(operators, Vector); 139 | expect(shim._binary("+", vec, vec, operators).contents[2]).toBe(6); 140 | expect(() => shim._binary("+", vec, 1, operators)).toThrowError(TypeError); 141 | expect(() => shim._binary("+", 1, vec, operators)).toThrowError(TypeError); 142 | expect(shim._binary("+", 1, 1, operators)).toBe(2); 143 | }); 144 | }); 145 | 146 | describe('overloading on the right', () => { 147 | 148 | const Ops = shim.Operators({ }, { left: Number, 149 | '*'(a, b) { 150 | return new Vector(b.contents.map(elt => a * elt)); 151 | } 152 | }); 153 | 154 | class Vector extends Ops { 155 | constructor(contents) { super(); this.contents = contents; } 156 | } 157 | 158 | const vec = new Vector([1, 2, 3]); 159 | 160 | it('* throws when not in operator set', () => { 161 | const operators = shim._declareOperators(); 162 | expect(() => shim._binary('*', 2, vec, operators)).toThrowError(TypeError); 163 | }); 164 | 165 | it('Number*Vector is permitted, other combinations banned', () => { 166 | const operators = shim._declareOperators(); 167 | shim._withOperatorsFrom(operators, Vector); 168 | expect(shim._binary('*', 2, vec, operators).contents[2]).toBe(6); 169 | expect(() => shim._binary('*', vec, vec, operators)).toThrowError(TypeError); 170 | expect(() => shim._binary('*', vec, 2, operators)).toThrowError(TypeError); 171 | expect(shim._binary('*', 2, 2, operators)).toBe(4); 172 | }); 173 | }); 174 | 175 | describe('overloading on the left', () => { 176 | 177 | const Ops = shim.Operators({ }, { right: Number, 178 | '*'(a, b) { 179 | return new Vector(a.contents.map(elt => b * elt)); 180 | } 181 | }); 182 | 183 | class Vector extends Ops { 184 | constructor(contents) { super(); this.contents = contents; } 185 | } 186 | 187 | const vec = new Vector([1, 2, 3]); 188 | 189 | it('* throws when not in operator set', () => { 190 | const operators = shim._declareOperators(); 191 | expect(() => shim._binary('*', 2, vec, operators)).toThrowError(TypeError); 192 | }); 193 | 194 | it('Number*Vector is permitted, other combinations banned', () => { 195 | const operators = shim._declareOperators(); 196 | shim._withOperatorsFrom(operators, Vector); 197 | expect(() => shim._binary('*', 2, vec, operators)).toThrowError(TypeError); 198 | expect(() => shim._binary('*', vec, vec, operators)).toThrowError(TypeError); 199 | expect(shim._binary('*', vec, 2, operators).contents[2]).toBe(6); 200 | expect(shim._binary('*', 2, 2, operators)).toBe(4); 201 | }); 202 | }); 203 | 204 | describe('[] overloading', () => { 205 | const Ops = shim.Operators({ 206 | '[]'(a, b) { 207 | return a.contents[b]; 208 | }, 209 | '[]='(a, b, c) { 210 | a.contents[b] = c; 211 | } 212 | }); 213 | 214 | class Vector extends Ops { 215 | constructor(contents) { super(); this.contents = contents; } 216 | get length() { return this.contents.length; } 217 | } 218 | 219 | 220 | it('Vector[Number] access works', () => { 221 | const vec = new Vector([1, 2, 3]); 222 | expect(vec[0]).toBe(1); 223 | expect(vec[1]).toBe(2); 224 | expect(vec[2]).toBe(3); 225 | expect(vec[3]).toBe(undefined); 226 | expect(vec[-1]).toBe(undefined); 227 | expect(vec[.5]).toBe(undefined); 228 | expect(vec.contents[1]).toBe(2); 229 | expect(vec["-0"]).toBe(undefined); 230 | expect(vec.length).toBe(3); 231 | expect(Object.getPrototypeOf(vec)).toBe(Vector.prototype); 232 | }); 233 | 234 | it('Vector[Number] = value access works', () => { 235 | 'use strict'; 236 | const vec = new Vector([1, 2, 3]); 237 | expect(vec[0]).toBe(1); 238 | expect(vec[0] = 5).toBe(5); 239 | expect(vec[0]).toBe(5); 240 | 241 | expect(vec[1]).toBe(2); 242 | expect(vec[1] = 20).toBe(20); 243 | expect(vec[1]).toBe(20); 244 | 245 | expect(vec[5]).toBe(undefined); 246 | expect(vec[5] = 25).toBe(25); 247 | expect(vec[5]).toBe(25); 248 | 249 | expect(vec[.5]).toBe(undefined); 250 | expect(() => vec[.5] = 25).toThrowError(TypeError); 251 | expect(vec[.5]).toBe(undefined); 252 | 253 | expect(vec[-1]).toBe(undefined); 254 | expect(() => vec[-1] = 25).toThrowError(TypeError); 255 | expect(vec[-1]).toBe(undefined); 256 | 257 | expect(vec["-0"]).toBe(undefined); 258 | expect(() => vec["-0"] = 25).toThrowError(TypeError); 259 | expect(vec["-0"]).toBe(undefined); 260 | }); 261 | 262 | it('in works', () => { 263 | const vec = new Vector([1, 2, 3]); 264 | expect(0 in vec).toBe(true); 265 | expect(1 in vec).toBe(true); 266 | expect(2 in vec).toBe(true); 267 | expect("0" in vec).toBe(true); 268 | expect("1" in vec).toBe(true); 269 | expect("2" in vec).toBe(true); 270 | expect("contents" in vec).toBe(true); 271 | 272 | expect(-1 in vec).toBe(false); 273 | expect(.5 in vec).toBe(false); 274 | expect("-0" in vec).toBe(false); 275 | expect(3 in vec).toBe(false); 276 | }); 277 | 278 | it('keys works', () => { 279 | const vec = new Vector([1, 2, 3]); 280 | expect(Object.getOwnPropertyNames(vec)).toEqual(["0", "1", "2", "contents"]); 281 | }); 282 | 283 | it('defineOwnProperty and getOwnProperty work', () => { 284 | const vec = new Vector([1, 2, 3]); 285 | Object.defineProperty(vec, "3", { value: 5, writable: true, enumerable: true, configurable: false }); 286 | expect(Object.getOwnPropertyDescriptor(vec, "3")).toEqual({ value: 5, writable: true, enumerable: true, configurable: false }); 287 | Object.defineProperty(vec, "foobar", { value: 5, writable: false, enumerable: false, configurable: false }); 288 | expect(Object.getOwnPropertyDescriptor(vec, "foobar")).toEqual({ value: 5, writable: false, enumerable: false, configurable: false }); 289 | expect(() => Object.defineProperty(vec, "2", { writable: false, enumerable: true, configurable: false, value: 1 })).toThrowError(TypeError); 290 | expect(() => Object.defineProperty(vec, "2", { writable: true, enumerable: false, configurable: false, value: 1 })).toThrowError(TypeError); 291 | expect(() => Object.defineProperty(vec, "2", { writable: true, enumerable: true, configurable: true, value: 1 })).toThrowError(TypeError); 292 | Object.defineProperty(vec, "2", { writable: true, enumerable: true, configurable: false, value: 1 }) 293 | expect(vec[2]).toBe(1); 294 | }); 295 | }); 296 | 297 | describe("Open set handling", () => { 298 | it("works for +", () => { 299 | const OpsA = shim.Operators({ 300 | open: ["+"] 301 | }); 302 | const a = new OpsA; 303 | 304 | const OpsB = shim.Operators({ }, { 305 | left: OpsA, 306 | '+'(a, b) { return 3; } 307 | }); 308 | const b = new OpsB; 309 | 310 | const operators = shim._declareOperators(); 311 | shim._withOperatorsFrom(operators, OpsA, OpsB); 312 | expect(shim._binary("+", a, b, operators)).toBe(3); 313 | }); 314 | }); 315 | -------------------------------------------------------------------------------- /src/transform/README.md: -------------------------------------------------------------------------------- 1 | This package implements an [operator overloading proposal](https://github.com/littledan/proposal-operator-overloading/) for JavaScript as a Babel plugin. 2 | 3 | To use the plugin, run the following commands for installation 4 | 5 | ```sh 6 | npm install --save-dev @littledan/plugin-transform-operator-overloading 7 | npm install --save-prod @littledan/operator-overloading-shim 8 | ``` 9 | 10 | and add the following to your `.babelrc`: 11 | 12 | ```js 13 | { 14 | "plugins": ["@littledan/plugin-transform-operator-overloading"] 15 | } 16 | ``` 17 | 18 | If you encounter any issues, including unexpected behavior, poor performance, weird ergonomics, etc, please [file an issue](https://github.com/littledan/proposal-operator-overloading/issues/new). 19 | 20 | ## Recommended best practices 21 | 22 | - Use `with operators from` declarations just in code that needs it, rather than at the top level of the module. This makes the transformation only apply to that code, reducing the predictability and performance impact. 23 | - When creating a library that exposes operator overloading, expose a method-based interface as well, to support usage without this transform. 24 | - Note that overloading [] or []= results in the creation of a Proxy; carefully consider whether this is appropriate for performance-sensitive code. 25 | 26 | ## Deviations from proto-specification behavior 27 | 28 | - Rather than using the syntax `with operators from ABC`, use the syntax `withOperatorsFrom(ABC)` 29 | - When outside of any block which has a `with operators from` declaration, this transform treats objects with overloaded operators as if they didn't have overloading (and therefore undergo coercion like objects), whereas the spec behavior would be to throw a TypeError. 30 | - The underlying operator-overloading-shim does not protect against introspection of symbols or monkey-patching in the environment. Error checking behavior may be somewhat weaker. 31 | -------------------------------------------------------------------------------- /src/transform/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@littledan/plugin-transform-operator-overloading", 3 | "version": "0.0.1", 4 | "description": "Babel plugin for a transform for an operator overloading proposal", 5 | "main": "build/plugin.js", 6 | "scripts": { 7 | "test": "jasmine plugin.spec.js", 8 | "build": "babel plugin.js -d build" 9 | }, 10 | "repository": "https://github.com/littledan/proposal-operator-overloading/tree/master/src/transform", 11 | "keywords": [ 12 | "operator-overloading", 13 | "tc39", 14 | "babel-plugin" 15 | ], 16 | "author": "Daniel Ehrenberg", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/littledan/proposal-operator-overloading/issues" 20 | }, 21 | "homepage": "https://github.com/littledan/proposal-operator-overloading/blob/master/src/transform/README.md", 22 | "devDependencies": { 23 | "@babel/cli": "^7.2.3", 24 | "@babel/preset-env": "^7.2.3", 25 | "jasmine": "^3.3.1" 26 | }, 27 | "dependencies": { 28 | "@babel/core": "^7.2.2", 29 | "@babel/helper-plugin-utils": "^7.0.0", 30 | "@littledan/operator-overloading-shim": "0.0.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/transform/plugin.js: -------------------------------------------------------------------------------- 1 | // Babel plugin for operator overloading 2 | 3 | // This plugin changes withOperatorsFrom() calls into declarations 4 | // of operator sets, and arithmetic operations into calls into the 5 | // operator overloading runtime, if they take place within a 6 | // withOperatorsFrom() scope. 7 | // Note that the proposal uses a `with operators from` statement instead; 8 | // this version doesn't use such new syntax to avoid the complexity 9 | // that comes with a parser change. 10 | 11 | // This plugin runs in a single visitor pass. 12 | // The this object has the following properties included in it: 13 | // { 14 | // shim: uid of the required shim module (undefined when stack empty) 15 | // stack: An Array of { 16 | // operators: uid of the current modules object 17 | // path: The path that owns this operator set 18 | // } 19 | // } 20 | // When the stack is empty, there's no operator overloading registered, 21 | // and no transformation is done until entering a block with a 22 | // withOperatorsFrom() call as a StatementExpressoin. 23 | // The behavior for each element is as follows: 24 | // Blocks and programs: 25 | // - Check for a top-level withOperatorsFrom() statement. If found, 26 | // - Require the shim if empty, and add this as the first statement 27 | // in the block. 28 | // - Make the next statement a declaration of a uid for the operators 29 | // variable, initialized based on the outer operators variable 30 | // found from the stack. let newuid = shim._declareOperators(olduid); 31 | // - Save the operators variable and this node on the stack 32 | // - As a post callback, pop the top of the stack if we pushed something. 33 | // Function calls: 34 | // - If the function is withOperatorsFrom: 35 | // - If the stack is empty, throw an error (was not at the top level). 36 | // - Otherwise, turn it into shim._withOperatorsFrom(operators, args...) 37 | // Operators: 38 | // - Replace all x= operators with the expanded var = var x arg form 39 | // - Replace all infix mathematical operators with calls to 40 | // _shim._binary(_operators) 41 | // - Replace unary operators with _shim._unary(_operators) 42 | // - Replace preincrement and postincrement operators with an 43 | // assignment and call to the pos or neg operators, with 44 | // _shim._unary(_operators) 45 | 46 | import { declare } from "@babel/helper-plugin-utils"; 47 | import { template, types as t } from "@babel/core"; 48 | import { parse } from "@babel/parser"; 49 | 50 | const withOperatorsFromTemplate = template(` 51 | SHIM._withOperatorsFrom(OPERATORS, ARGS) 52 | `); 53 | 54 | const requireShimTemplate = template(` 55 | const SHIM = require("@littledan/operator-overloading-shim"); 56 | `); 57 | 58 | const declareOperatorsTemplate = template(` 59 | const OPERATORS = SHIM._declareOperators(OUTER); 60 | `); 61 | 62 | const unaryOperatorTemplate = template(` 63 | SHIM._unary(OPERATOR, EXPRESSION, OPERATORS) 64 | `); 65 | 66 | const binaryOperatorTemplate = template(` 67 | SHIM._binary(OPERATOR, LEFT, RIGHT, OPERATORS) 68 | `); 69 | 70 | const preIncrementTemplate = template(` 71 | EXPRESSION = SHIM._unary(OPERATOR, EXPRESSION, OPERATORS) 72 | `); 73 | 74 | const postIncrementTemplate = template(` 75 | TEMPORARY = EXPRESSION, 76 | EXPRESSION = SHIM._unary(OPERATOR, EXPRESSION, OPERATORS), 77 | TEMPORARY 78 | `); 79 | 80 | function isWithOperatorsFrom(node) { 81 | return t.isIdentifier(node.callee) && node.callee.name === "withOperatorsFrom"; 82 | } 83 | 84 | const visitBlockStatementLike = { 85 | enter(path) { 86 | if (!path.node.body.some(statement => 87 | t.isExpressionStatement(statement) && 88 | t.isCallExpression(statement.expression) && 89 | isWithOperatorsFrom(statement.expression))) return; 90 | const prelude = []; 91 | if (this.shim === undefined) { 92 | this.shim = path.scope.generateUidIdentifier("shim"); 93 | prelude.push(requireShimTemplate({SHIM: this.shim})); 94 | } 95 | const operators = path.scope.generateUidIdentifier("operators"); 96 | const outer = this.inactive() ? undefined : this.peek().operators; 97 | this.stack.push({operators, path}); 98 | prelude.push(declareOperatorsTemplate({ 99 | OPERATORS: operators, 100 | SHIM: this.shim, 101 | OUTER: outer, 102 | })); 103 | path.unshiftContainer('body', prelude); 104 | }, 105 | exit(path) { 106 | if (this.peek() && (this.peek().path === path)) { 107 | this.stack.pop(); 108 | if (this.inactive()) this.shim = undefined; 109 | } 110 | } 111 | } 112 | 113 | const fixedBinaryOperators = new Set(["===", "!==", "in", "instanceOf"]); 114 | 115 | export default declare(api => { 116 | api.assertVersion(7); 117 | 118 | return { 119 | pre() { 120 | this.stack = []; 121 | this.peek = () => this.stack[this.stack.length - 1]; 122 | this.inactive = () => this.stack.length === 0; 123 | }, 124 | post() { 125 | if (!this.inactive() || this.shim !== undefined) { 126 | throw "internal error"; 127 | } 128 | }, 129 | visitor: { 130 | BlockStatement: visitBlockStatementLike, 131 | Program: visitBlockStatementLike, 132 | CallExpression(path) { 133 | if (!isWithOperatorsFrom(path.node)) return; 134 | if (this.inactive()) { 135 | throw path.buildCodeFrameError( 136 | "withOperatorsFrom calls must be statements, not nested expressions."); 137 | } 138 | const uid = this.peek().operators; 139 | path.replaceWith(withOperatorsFromTemplate({ 140 | SHIM: this.shim, 141 | OPERATORS: uid, 142 | ARGS: path.node.arguments 143 | })); 144 | }, 145 | UpdateExpression(path) { 146 | if (this.inactive()) return; 147 | let statement; 148 | if (path.node.prefix) { 149 | statement = preIncrementTemplate({ 150 | SHIM: this.shim, 151 | OPERATOR: t.StringLiteral(path.node.operator), 152 | EXPRESSION: path.node.argument, 153 | OPERATORS: this.peek().operators, 154 | }); 155 | } else { 156 | let temporary = path.scope.generateUidIdentifier("t"); 157 | statement = postIncrementTemplate({ 158 | SHIM: this.shim, 159 | OPERATOR: t.StringLiteral(path.node.operator), 160 | EXPRESSION: path.node.argument, 161 | OPERATORS: this.peek().operators, 162 | TEMPORARY: temporary, 163 | }); 164 | } 165 | path.replaceWith(statement); 166 | }, 167 | UnaryExpression(path) { 168 | if (this.inactive()) return; 169 | const operator = { "+": "pos", "-": "neg", "~": "~"}[path.node.operator]; 170 | if (operator === undefined) return; 171 | path.replaceWith(unaryOperatorTemplate({ 172 | SHIM: this.shim, 173 | OPERATOR: t.StringLiteral(operator), 174 | EXPRESSION: path.node.argument, 175 | OPERATORS: this.peek().operators, 176 | })); 177 | }, 178 | BinaryExpression(path) { 179 | if (this.inactive()) return; 180 | if (fixedBinaryOperators.has(path.node.operator)) return; 181 | path.replaceWith(binaryOperatorTemplate({ 182 | SHIM: this.shim, 183 | OPERATOR: t.StringLiteral(path.node.operator), 184 | LEFT: path.node.left, 185 | RIGHT: path.node.right, 186 | OPERATORS: this.peek().operators, 187 | })); 188 | }, 189 | AssignmentExpression(path) { 190 | if (this.inactive()) return; 191 | // Desugar assignment expressions so the visitor 192 | // can implement operator overloading 193 | const operator = { 194 | "+=": "+", 195 | "-=": "-", 196 | "*=": "*", 197 | "/=": "/", 198 | "%=": "%", 199 | "<<=": "<<", 200 | ">>=": ">>", 201 | ">>>=": ">>>", 202 | "|=": "|", 203 | "^=": "^", 204 | "&=": "&", 205 | }[path.node.operator]; 206 | if (operator === undefined) return; 207 | const newValue = t.BinaryOperator(operator, path.node.left, path.node.right); 208 | path.node.right = newValue; 209 | path.node.operator = "="; 210 | }, 211 | } 212 | }; 213 | }); 214 | -------------------------------------------------------------------------------- /src/transform/plugin.spec.js: -------------------------------------------------------------------------------- 1 | const babel = require("@babel/core"); 2 | const shim = require("@littledan/operator-overloading-shim"); 3 | 4 | const debug = false; 5 | 6 | function transform(code) { 7 | code = babel.transform(code, { plugins: ["./build/plugin.js"] }).code; 8 | if (debug) console.log(code); 9 | return code; 10 | } 11 | 12 | describe("overloading + works", () => { 13 | const Ops = shim.Operators({ 14 | '+'(a, b) { 15 | return new Vector(a.contents.map((elt, i) => elt + b.contents[i])); 16 | } 17 | }); 18 | 19 | class Vector extends Ops { 20 | constructor(contents) { super(); this.contents = contents; } 21 | } 22 | 23 | it("simply works", () => { 24 | const code = transform(` 25 | withOperatorsFrom(Vector); 26 | const vec = new Vector([1, 2, 3]); 27 | const vec2 = vec + vec; 28 | val = vec2.contents[2]; 29 | `); 30 | let val; 31 | eval(code); 32 | expect(val).toBe(6); 33 | }); 34 | }); 35 | 36 | describe("test everything on a full wrapper of Numbers (no interoperation)", () => { 37 | const Ops = shim.Operators({ 38 | '-'(a, b) { return a.n - b.n; }, 39 | '*'(a, b) { return a.n * b.n; }, 40 | '/'(a, b) { return a.n / b.n; }, 41 | '%'(a, b) { return a.n % b.n; }, 42 | '**'(a, b) { return a.n ** b.n; }, 43 | '&'(a, b) { return a.n & b.n; }, 44 | '^'(a, b) { return a.n ^ b.n; }, 45 | '|'(a, b) { return a.n | b.n; }, 46 | '<<'(a, b) { return a.n << b.n; }, 47 | '>>'(a, b) { return a.n >> b.n; }, 48 | '>>>'(a, b) { return a.n >>> b.n; }, 49 | '=='(a, b) { return a.n == b.n; }, 50 | '+'(a, b) { return a.n + b.n; }, 51 | '<'(a, b) { return a.n < b.n; }, 52 | 'pos'(a) { return +a.n; }, 53 | 'neg'(a) { return -a.n; }, 54 | '++'(a) { let x = a.n; return new MyNum(++x); }, 55 | '--'(a) { let x = a.n; return new MyNum(--x); }, 56 | '~'(a) { return ~a.n; }, 57 | }); 58 | 59 | class MyNum extends Ops { 60 | constructor(n) { super(); this.n = n; } 61 | } 62 | 63 | it("works", () => { 64 | eval(transform(` 65 | let x = new MyNum(2); 66 | let y = new MyNum(3); 67 | 68 | expect(() => x+y).toThrowError(TypeError); 69 | expect(() => x-y).toThrowError(TypeError); 70 | 71 | withOperatorsFrom(MyNum); 72 | 73 | expect(x+y).toBe(5); 74 | expect(x-y).toBe(-1); 75 | expect(x*y).toBe(6); 76 | expect(x/y).toBe(2/3); 77 | expect(x%y).toBe(2); 78 | expect(x**y).toBe(8); 79 | expect(x&y).toBe(2); 80 | expect(x^y).toBe(1); 81 | expect(x|y).toBe(3); 82 | expect(x<>y).toBe(0); 84 | expect(x>>>y).toBe(0); 85 | expect(x==y).toBe(false); 86 | expect(xy).toBe(false); 88 | expect(x>y).toBe(false); 89 | expect(+x).toBe(2); 90 | expect(-x).toBe(-2); 91 | expect(~x).toBe(-3); 92 | expect((x++).n).toBe(2); 93 | expect(x.n).toBe(3); 94 | expect((x--).n).toBe(3); 95 | expect(x.n).toBe(2); 96 | expect((++x).n).toBe(3); 97 | expect(x.n).toBe(3); 98 | expect((--x).n).toBe(2); 99 | expect(x.n).toBe(2); 100 | `)); 101 | }); 102 | }); 103 | 104 | describe("nested scopes", () => { 105 | const OpsA = shim.Operators({ 106 | 'pos'(a) { return 1; }, 107 | open: ["+"] 108 | }); 109 | const a = new OpsA; 110 | 111 | const OpsB = shim.Operators({ 112 | 'pos'(b) { return 2; } 113 | }, { left: OpsA, 114 | '+'(a, b) { return 3; } 115 | }); 116 | const b = new OpsB; 117 | 118 | it("throws appropriate errors in straight line code", () => { 119 | eval(transform(` 120 | expect(() => +a).toThrowError(TypeError); 121 | expect(() => +b).toThrowError(TypeError); 122 | expect(() => a+b).toThrowError(TypeError); 123 | 124 | withOperatorsFrom(OpsA); 125 | 126 | expect(+a).toBe(1); 127 | expect(() => +b).toThrowError(TypeError); 128 | expect(() => a+b).toThrowError(TypeError); 129 | 130 | withOperatorsFrom(OpsB); 131 | 132 | expect(+a).toBe(1); 133 | expect(+b).toBe(2); 134 | expect(a+b).toBe(3); 135 | `)); 136 | 137 | eval(transform(` 138 | expect(() => +a).toThrowError(TypeError); 139 | expect(() => +b).toThrowError(TypeError); 140 | expect(() => a+b).toThrowError(TypeError); 141 | 142 | withOperatorsFrom(OpsB); 143 | 144 | expect(() => +a).toThrowError(TypeError); 145 | expect(+b).toBe(2); 146 | expect(() => a+b).toThrowError(TypeError); 147 | 148 | withOperatorsFrom(OpsA); 149 | 150 | expect(+a).toBe(1); 151 | expect(+b).toBe(2); 152 | expect(a+b).toBe(3); 153 | `)); 154 | 155 | eval(transform(` 156 | expect(() => +a).toThrowError(TypeError); 157 | expect(() => +b).toThrowError(TypeError); 158 | expect(() => a+b).toThrowError(TypeError); 159 | 160 | withOperatorsFrom(OpsA, OpsB); 161 | 162 | expect(+a).toBe(1); 163 | expect(+b).toBe(2); 164 | expect(a+b).toBe(3); 165 | `)); 166 | }); 167 | 168 | it("throws appropriate errors in nested code", () => { 169 | eval(transform(` 170 | expect(() => +a).toThrowError(TypeError); 171 | expect(() => +b).toThrowError(TypeError); 172 | expect(() => a+b).toThrowError(TypeError); 173 | 174 | withOperatorsFrom(OpsA); 175 | 176 | { 177 | expect(+a).toBe(1); 178 | expect(() => +b).toThrowError(TypeError); 179 | expect(() => a+b).toThrowError(TypeError); 180 | 181 | withOperatorsFrom(OpsB); 182 | 183 | expect(+a).toBe(1); 184 | expect(+b).toBe(2); 185 | expect(a+b).toBe(3); 186 | } 187 | 188 | expect(+a).toBe(1); 189 | expect(() => +b).toThrowError(TypeError); 190 | expect(() => a+b).toThrowError(TypeError); 191 | `)); 192 | }); 193 | 194 | }); 195 | --------------------------------------------------------------------------------