├── .editorconfig ├── .gitattributes ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── docs ├── design.md ├── extensions.md ├── syntax.md └── usecases.md ├── experimental ├── Extension.js ├── binary.js ├── common │ ├── InternalSlot.js │ ├── abstract.js │ └── extra.js ├── index.js ├── package.json ├── ternary.js └── test │ └── index.js ├── index.html ├── package.json └── spec.emu /.editorconfig: -------------------------------------------------------------------------------- 1 | # See https://github.com/hax/dotfiles/tree/master/src/.editorconfig for commented edition 2 | root = true 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 3 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | trim_trailing_newlines = true 11 | max_line_length = 80 12 | [*.{yaml,yml}] 13 | indent_style = space 14 | indent_size = 2 15 | [*.{markdown,md}] 16 | indent_size = 4 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | index.html -diff merge=ours 2 | spec.js -diff merge=ours 3 | spec.css -diff merge=ours 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Only apps should have lockfiles 40 | yarn.lock 41 | package-lock.json 42 | npm-shrinkwrap.json 43 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ECMA TC39 and contributors 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extensions and `::` operator 2 | 3 | ## Proposal status 4 | 5 | This is an ECMAScript (JavaScript) proposal in stage 1. 6 | 7 | Note: The proposal could be seen as the reshape of the "virtual method" part of old [bind operator proposal](https://github.com/tc39/proposal-bind-operator), see https://github.com/tc39/proposal-bind-operator/issues/56. 8 | 9 | ## Simple examples 10 | 11 | ### Example of ad-hoc extension methods and accessors 12 | ```js 13 | // define two extension methods 14 | const ::toArray = function () { return [...this] } 15 | const ::toSet = function () { return new Set(this) } 16 | 17 | // define a extension accessor 18 | const ::allDivs = { 19 | get() { return this.querySelectorAll('div') } 20 | } 21 | 22 | // reuse built-in prototype methods and accessors 23 | const ::flatMap = Array.prototype.flatMap 24 | const ::size = Object.getOwnPropertyDescriptor(Set.prototype, 'size') 25 | 26 | // Use extension methods and accesors to calculate 27 | // the count of all classes of div element. 28 | let classCount = document::allDivs 29 | ::flatMap(e => e.classList::toArray()) 30 | ::toSet()::size 31 | ``` 32 | 33 | roughly equals to: 34 | 35 | ```js 36 | // define two extension methods 37 | const $toArray = function () { return [...this] } 38 | const $toSet = function () { return new Set(this) } 39 | 40 | // define a extension accessor 41 | const $allDivs = { 42 | get() { return this.querySelectorAll('div') } 43 | } 44 | 45 | // reuse built-in prototype method and accessor 46 | const $flatMap = Array.prototype.flatMap 47 | const $size = Object.getOwnPropertyDescriptor(Set.prototype, 'size') 48 | 49 | // Use extension methods and accesors to calculate 50 | // the count of all classes of div element. 51 | let $ 52 | $ = $allDivs.get.call(document) 53 | $ = $flatMap.call($, e => $toArray.call(e.classList)) 54 | $ = $toSet.call($) 55 | $ = $size.get.call($) 56 | let classCount = $ 57 | ``` 58 | 59 | ### Example of using constructors or namespace object as extensions 60 | 61 | ```js 62 | // util.js 63 | export const toArray = iterable => [...iterable] 64 | export const toSet = iterable => new Set(iterable) 65 | ``` 66 | 67 | ```js 68 | import * as util from './util.js' 69 | 70 | const ::allDivs = { 71 | get() { return this.querySelectorAll('div') } 72 | } 73 | 74 | let classCount = document::allDivs 75 | ::Array:flatMap( 76 | e => e.classList::util:toArray()) 77 | ::util:toSet() 78 | ::Set:size 79 | ``` 80 | 81 | roughly equals to: 82 | 83 | ```js 84 | import * as util from './util.js' 85 | 86 | const $allDivs = { 87 | get() { return this.querySelectorAll('div') } 88 | } 89 | 90 | let $ 91 | $ = $allDivs.get.call(document) 92 | $ = Array.prototype.flatMap.call($, 93 | e => util.toArray(e.classList)) 94 | $ = util.toSet($) 95 | $ = Object.getOwnPropertyDescriptor(Set.prototype, 'size').get.call($) 96 | let classCount = $ 97 | ``` 98 | 99 | ## Changes of the old bind operator proposal 100 | 101 | - keep `obj::foo()` syntax for extension methods 102 | - repurpose `obj::foo` as extension getters and add `obj::foo =` as extension setters 103 | - separate namespace for ad-hoc extension methods and accessors, do not pollute normal binding names 104 | - add `obj::ext:name` syntax 105 | - change operator precedence to same as `.` 106 | - remove `::obj.foo` (use cases can be solved by custom extension + library, or other proposals) 107 | 108 | ## Other matrials 109 | 110 | - [design details](docs/design.md) 111 | - [slide for stage 1](https://johnhax.net/2020/tc39-nov-ext/slide) 112 | - [experimental implementation](experimental) 113 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # Design 2 | 3 | This proposal introduce `::` (double colon) infix notation which the left side is an expression and the right side could be either a simple identifier or two identifiers separated by `:` (single colon). `::` has the same precedence of `.` (member access) operator. 4 | 5 | This proposal also introduce a new global `Extension` function object and a new well-known symbol `Symbol.extension`. 6 | 7 | ## `x::name` (ad-hoc extension methods and accessors) 8 | 9 | ### Usage 10 | 11 | Declare a ad-hoc extension method and use it: 12 | ```js 13 | const ::last = function () { 14 | return this[this.length - 1] 15 | } 16 | 17 | [1, 2, 3]::last() // 3 18 | ``` 19 | 20 | Like normal const declarations, ad-hoc extension methods are also 21 | lexical: 22 | 23 | ```js 24 | let x = 'x' 25 | 26 | const ::foo = function () { return `${this}@outer` } 27 | 28 | x::foo() // 'x@outer' 29 | 30 | { 31 | // TDZ here 32 | // x::foo() 33 | 34 | const ::foo = function () { return `${this}@inner` } 35 | 36 | x::foo() // 'x@inner' 37 | } 38 | ``` 39 | 40 | Ad-hoc extension accessors: 41 | 42 | ```js 43 | const ::last = { 44 | get() { return this[this.length - 1] }, 45 | set(v) { this[this.length - 1] = v }, 46 | } 47 | 48 | let x = [1, 2, 3] 49 | x::last // 3 50 | x::last = 10 51 | x // [1, 2, 10] 52 | x::last++ 53 | x // [1, 2, 11] 54 | ``` 55 | 56 | `a::b` have the same precedence of `a.b`, so they can be chained seamlessly: 57 | 58 | ```js 59 | document.querySelectorAll('.note') 60 | ::filter(x => x.textContent.include(keyword)) 61 | .map() 62 | ``` 63 | 64 | 65 | 66 | ### Transpile 67 | 68 | ```js 69 | // declare a ad-hoc extension method/accessor 70 | const ::method = func 71 | const ::prop = {get: getter, set: setter} 72 | 73 | // use ad-hoc extension methods and accessors 74 | x::method(...args) 75 | x::prop 76 | x::prop = value 77 | ``` 78 | could be transpile to 79 | ```js 80 | const $method = CreateExtMethod(func) 81 | const $prop = CreateExtMethod({get: getter, set: setter}) 82 | 83 | CallExtMethod($method, x, args) // Call(func, x, args) 84 | CallExtGetter($prop, x) // Call(getter, x) 85 | CallExtSetter($prop, x, value) // Call(setter, x, [value]) 86 | ``` 87 | 88 | See [experimental implementation](../experimental/binary.js) for details. 89 | 90 | ## `x::ext:name` (extension) 91 | 92 | ```js 93 | x::O:method(...args) 94 | x::O:prop 95 | x::O:prop = value 96 | ``` 97 | roughly equals to 98 | ```js 99 | Call(GetOwnProperty(O.prototype, 'prop').value, x, args) 100 | Call(GetOwnProperty(O.prototype, 'prop').get, x) 101 | Call(GetOwnProperty(O.prototype, 'prop').set, x, [value]) 102 | ``` 103 | 104 | If `O` is not a constructor, `x::O:func(...args)` roughly equals to `O.func(x, ...args)`. 105 | 106 | The behavior can also be customized by `Symbol.extension`. 107 | 108 | See [experimental implementation](../experimental/ternary.js) for details. 109 | 110 | ## `Extension` API 111 | 112 | ```js 113 | class Extension { 114 | constructor(extensionMethods) 115 | static from(object) 116 | } 117 | ``` 118 | 119 | See [experimental implementation](../experimental/Extension.js) for details. 120 | 121 | ## Extra features (could be split to follow-on proposals) 122 | 123 | ### Declare/import multiple ad-hoc extension methods and accessors 124 | 125 | ```js 126 | const ::{x, y as x1} from value 127 | ``` 128 | work as 129 | ```js 130 | const $ext = Extension.from(value) 131 | const ::x = ExtensionGetMethod($ext, 'x') 132 | const ::x1 = ExtensionGetMethod($ext, 'y') 133 | ``` 134 | And 135 | ```js 136 | import ::{x, y as x1} from 'mod' 137 | ``` 138 | work as 139 | ```js 140 | import * as $mod from 'mod' 141 | const ::{x, y as x1} from $mod 142 | ``` 143 | 144 | ### Static helper methods of `Extension` 145 | 146 | ```js 147 | // doesn't work 148 | const ::cube = x => x ** 3 149 | const ::sqrt = Math.sqrt 150 | ``` 151 | ```js 152 | // work 153 | const ::cube = function () { 154 | return this ** 3 155 | } 156 | const ::sqrt = function () { 157 | return Math.sqrt(this) 158 | } 159 | ``` 160 | 161 | ```js 162 | // Use Extension.method helper 163 | const ::cube = Extension.method(x => x ** 3) 164 | 2::cube() // 8 165 | 166 | // Use Extension.accessor helper 167 | const ::sqrt = Extension.accessor(Math.sqrt) 168 | 9::sqrt // 3 169 | ``` 170 | 171 | `Extension.method` and `Extension.accessor` also accept the extra options argument. 172 | 173 | Programmers could use `"receiver"` option to indicate how to deal with the receiver. 174 | 175 | ```js 176 | // Define ::max extension accessor 177 | const ::max = Extension.accessor(Math.max, {receiver: 'spread'}); 178 | // spread the receiver, so `receiver::max` work as `Math.max(...receiver)` 179 | 180 | [1, 2, 3]::max // 3 181 | ``` 182 | 183 | The valid values of `"receiver"` option: 184 | #### `'first'` (default) 185 | `Extension.method(f, {receiver: 'first'})` behave like 186 | ```js 187 | function (...args) { return f(this, ...args) } 188 | ``` 189 | #### `'last'` 190 | `Extension.method(f, {receiver: 'last'})` behave like 191 | ```js 192 | function (...args) { return f(...args, this) } 193 | ``` 194 | #### `'spread first'` or `'first spread'` or `'spread'` 195 | `Extension.method(f, {receiver: 'spread first'})` behave like 196 | ```js 197 | function (...args) { return f(...this, ...args) } 198 | ``` 199 | #### `'spread last'` or `'last spread'` 200 | `Extension.method(f, {receiver: 'spread last'})` behave like 201 | ```js 202 | function (...args) { return f(...args, ...this) } 203 | ``` 204 | 205 | ### Optional chaining 206 | 207 | ```js 208 | x?::extMethod() 209 | x?::ext:method() 210 | ``` 211 | 212 | work as 213 | 214 | ```js 215 | x === null || x === undefined ? undefined : x::extMethod() 216 | x === null || x === undefined ? undefined : x::ext:method() 217 | ``` 218 | 219 | ## Other possible features 220 | 221 | ### in-place extension methods/accessors 222 | 223 | Just like dot notation `obj.prop` has corresponding bracket notation `obj[computedProp]`, we could also introduce similar syntax for extension methods/accessors. 224 | 225 | ```js 226 | x::[expression] 227 | ``` 228 | work as 229 | ```js 230 | const ::$extMethodOrAccessor = expression 231 | x::$extMethodOrAccessor 232 | ``` 233 | 234 | Currently the author of the proposal feel this syntax do not very useful, so not include it. If there are strong use cases, we could add it back. 235 | -------------------------------------------------------------------------------- /docs/extensions.md: -------------------------------------------------------------------------------- 1 | # Extensions in other programming languages 2 | 3 | ## What is Extensions? 4 | 5 | [Extensions](https://en.wikipedia.org/wiki/Extension_method) is a feature many programming languages introduced in recent years, especially the new languages Swift/Kotlin/Dart. 6 | 7 | Note: we use term "extensions" to denote "static extensions" which excludes [monkey patch](https://en.wikipedia.org/wiki/Monkey_patch). 8 | 9 | It allows the developers adding methods to built-in libraries and 3rd-party libraries *statically* and *locally*, and also provide a new way to organize the code. Some programming languages already use Extensions to envlove their standard libraries. 10 | 11 | The main difference of Extensions of other programming languages with Extensions of JavaScript (aka. this proposal) is, this proposal use notation `::` instead of overloading `.`, see [syntax](syntax.md) for further discussion. A consequence of that is this proposal has a very simple semantic of method dispatching instead of complex resolving mechanism. 12 | 13 | ### History of adoption of Extensions or similar features in the programming languages 14 | 15 | - 2003: Classboxes [Classboxes](http://scg.unibe.ch/research/classboxes), 16 | [paper](http://scg.unibe.ch/archive/papers/Berg03aClassboxes.pdf) 17 | - 2005: Groovy [Categories](https://groovy-lang.org/metaprogramming.html#categories) 18 | - 2007: C# 3.0+ [Extension methods](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods) 19 | (and possible future [extension properties](https://stackoverflow.com/questions/619033/does-c-sharp-have-extension-properties), 20 | [issue](https://github.com/dotnet/csharplang/issues/192)) 21 | - 2007: VB.NET 9.0+ [Extension Methods](https://docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/language-features/procedures/extension-methods) 22 | - [2010](https://bugs.ruby-lang.org/issues/4085), 2013: Ruby 2.0+ [Refinements (local class extensions)](https://bugs.ruby-lang.org/projects/ruby-master/wiki/RefinementsSpec) 23 | - 2011: Kotlin [Extensions](https://kotlinlang.org/docs/reference/extensions.html) 24 | - 2012: Lombok v0.11.2+ [Extension Method](https://projectlombok.org/features/experimental/ExtensionMethod) 25 | - 2013: Haxe 3+ [Static Extension](https://haxe.org/manual/lf-static-extension.html) 26 | - 2013: Scala 2.10+ [Implicit Classes](https://docs.scala-lang.org/overviews/core/implicit-classes.html) 27 | - 2014: Gosu [Enhancements](https://gosu-lang.github.io/docs.html#enhancements) 28 | - 2014: Swift [Extensions](https://docs.swift.org/swift-book/LanguageGuide/Extensions.html) 29 | - 201?: Eclipse Xtend [Extension Methods](http://www.eclipse.org/xtend/documentation/202_xtend_classes_members.html#extension-methods) 30 | - 2017?: Manifold [Java Extensions](https://github.com/manifold-systems/manifold/tree/master/manifold-deps-parent/manifold-ext#extension-classes-via-extension) 31 | - 2019: Dart 2.7+ [Extension Methods](https://dart.dev/guides/language/extension-methods) 32 | - 2021: Scala 3 [Extension Methods](https://dotty.epfl.ch/docs/reference/contextual/extension-methods.html) 33 | 34 | ### C# 35 | 36 | [C# Extension methods](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods) 37 | 38 | ```csharp 39 | using System.Linq; 40 | using System.Text; 41 | using System; 42 | 43 | namespace CustomExtensions 44 | { 45 | // Extension methods must be defined in a static class. 46 | public static class StringExtension 47 | { 48 | // This is the extension method. 49 | // The first parameter takes the "this" modifier 50 | // and specifies the type for which the method is defined. 51 | public static int WordCount(this String str) 52 | { 53 | return str.Split(new char[] {' ', '.','?'}, StringSplitOptions.RemoveEmptyEntries).Length; 54 | } 55 | } 56 | } 57 | namespace Extension_Methods_Simple 58 | { 59 | // Import the extension method namespace. 60 | using CustomExtensions; 61 | class Program 62 | { 63 | static void Main(string[] args) 64 | { 65 | string s = "The quick brown fox jumped over the lazy dog."; 66 | // Call the method as if it were an 67 | // instance method on the type. Note that the first 68 | // parameter is not specified by the calling code. 69 | int i = s.WordCount(); 70 | System.Console.WriteLine("Word count of s is {0}", i); 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | ### Swift 77 | 78 | [Swift Extensions](https://docs.swift.org/swift-book/LanguageGuide/Extensions.html) 79 | 80 | ```swift 81 | extension Int { 82 | var simpleDescription: String { 83 | return "The number \(self)" 84 | } 85 | } 86 | print(7.simpleDescription) 87 | // Prints "The number 7" 88 | ``` 89 | 90 | ### Kotlin 91 | 92 | [Kotlin Extensions](https://kotlinlang.org/docs/reference/extensions.html) 93 | 94 | ```kt 95 | open class Shape 96 | 97 | class Rectangle: Shape() 98 | 99 | fun Shape.getName() = "Shape" 100 | 101 | fun Rectangle.getName() = "Rectangle" 102 | 103 | fun printClassName(s: Shape) { 104 | println(s.getName()) 105 | } 106 | 107 | printClassName(Rectangle()) 108 | ``` 109 | 110 | ### Scala 111 | 112 | [Scala Implicits](https://docs.scala-lang.org/overviews/core/implicit-classes.html) 113 | 114 | ```scala 115 | object Helpers { 116 | implicit class IntWithTimes(x: Int) { 117 | def times[A](f: => A): Unit = { 118 | def loop(current: Int): Unit = 119 | if(current > 0) { 120 | f 121 | loop(current - 1) 122 | } 123 | loop(x) 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | ```scala 130 | import Helpers._ 131 | 5 times println("HI") 132 | ``` 133 | 134 | ### Dart 135 | 136 | [Dart Extension methods](https://dart.dev/guides/language/extension-methods) 137 | 138 | ```dart 139 | extension NumberParsing on String { 140 | int parseInt() { 141 | return int.parse(this); 142 | } 143 | // ··· 144 | } 145 | ``` 146 | 147 | ```dart 148 | import 'string_apis.dart'; 149 | // ··· 150 | print('42'.parseInt()); // Use an extension method. 151 | ``` 152 | 153 | ### Ruby 154 | 155 | [Ruby Refinements](https://bugs.ruby-lang.org/projects/ruby-master/wiki/RefinementsSpec) 156 | 157 | ```ruby 158 | class C 159 | def foo 160 | puts "C#foo" 161 | end 162 | end 163 | 164 | module M 165 | refine C do 166 | def foo 167 | puts "C#foo in M" 168 | end 169 | end 170 | end 171 | ``` 172 | 173 | ```ruby 174 | using M 175 | x = C.new 176 | c.foo #=> C#foo in M 177 | ``` 178 | 179 | See [Ruby 2.0 Refinements in Practice](https://yehudakatz.com/2010/11/30/ruby-2-0-refinements-in-practice/) written by Yehuda Katz. 180 | 181 | ## The discussion of introducing Extensions in TypeScript 182 | 183 | - https://github.com/microsoft/TypeScript/issues/9 184 | 185 | The main reason why TS can't introduce Extensions is, TS has a design goal that not generate special code or do runtime dispatching based on type. So it's hard to deal with the code like: 186 | 187 | ```ts 188 | interface Test { 189 | test(): string 190 | } 191 | 192 | // use similar syntax of Swift to declare extensions 193 | extension String { 194 | test() { return 'test on string' } 195 | } 196 | extension Number { 197 | test() { return 'test on number' } 198 | } 199 | 200 | const n = 1, s = '1', o = { test() { return 'test' } } 201 | 202 | n.test() // expect 'test on number' 203 | s.test() // expect 'test on string' 204 | o.test() // expect 'test' 205 | 206 | Number.prototype.test = function () { return 'test on number proto' } 207 | n.test() // behavior? what will generated code look like? 208 | 209 | for (const x of [n, s, o]) { 210 | // type of x is number | string | typeof o 211 | x.test() // behavior? what will generated code look like? 212 | const x1 = x as Test 213 | x1.test() // behavior? what will generated code look like? 214 | } 215 | ``` 216 | 217 | ## Can we overload `.` operator like other programming languages? 218 | 219 | All other programming languages overload `.` operator for extensions. But it seems very impossible to work in JavaScript: 220 | 221 | ```js 222 | let a = {} 223 | let b = { 224 | test() { return 'b'} 225 | } 226 | 227 | a.test() // throw 228 | b.test() // 'b' 229 | 230 | { 231 | // imaginary syntax to define an extension method locally 232 | const *.test = function () { return 'ext' } 233 | 234 | a.test() // 'ext' 235 | b.test() // 'b' 236 | 237 | a.test // ? 238 | 'test' in a // ? 239 | a.test = function () { return 'a' } // ? 240 | a.test() // 'a' ? 241 | 242 | delete a.test 243 | a.test() // 'ext' again? 244 | } 245 | ``` 246 | 247 | Obviously it will change the `.` semantic dramatically, and might break many optimization in current engines. 248 | 249 | A possible solution is make local extension have high preference (like Ruby) 250 | 251 | ```js 252 | let a = {} 253 | let b = { 254 | test() { return 'b'} 255 | } 256 | 257 | a.test() // throw 258 | b.test() // 'b' 259 | 260 | { 261 | // define an extension method 262 | const *.test = function () { return 'ext' } 263 | 264 | a.test() // 'ext' 265 | b.test() // 'ext' 266 | 267 | a.test // throw 268 | a.test = function () { return 'a' } // throw 269 | delete a.test // throw 270 | 271 | // do not overload `in` and `[]` 272 | 'test' in a // false 273 | a['test'] // undefined 274 | b['test']() // 'b' 275 | } 276 | ``` 277 | 278 | This is possible, but it seems do not have much benefit compare to a separate notation. 279 | -------------------------------------------------------------------------------- /docs/syntax.md: -------------------------------------------------------------------------------- 1 | # Syntax Consideration 2 | 3 | ## Alternative syntax for ad-hoc extension methods/accessors declaration 4 | 5 | ### Alternative 1 6 | 7 | ```js 8 | // explicit receiver parameter 9 | const iterable::toArray() { 10 | return [...iterable] 11 | } 12 | 13 | const indexed::last { 14 | // accessor group syntax 15 | get() { return indexed[indexed.length - 1]} 16 | set(v) { indexed[indexed.length - 1] = v } 17 | } 18 | 19 | // use `*` as placeholder of receiver, could also consider `_` or `?`, etc. 20 | const *::map = Array.prototype.map 21 | const *::size = Object.getOwnPropertyDescriptor(Set.prototype, 'size') 22 | ``` 23 | 24 | Issue: can't find satisfying syntax for TypeScript. 25 | 26 | ### Alternative 2 27 | 28 | ```js 29 | // align with possible future const function syntax 30 | const ::toArray() { 31 | return [...this] 32 | } 33 | 34 | const ::last { 35 | // accessor group syntax 36 | get() { return this[this.length - 1] } 37 | set(v) { this[this.length - 1] = v } 38 | } 39 | 40 | // no change 41 | const ::map = Array.prototype.map 42 | const ::size = Object.getOwnPropertyDescriptor(Set.prototype, 'size') 43 | ``` 44 | 45 | TypeScript: 46 | ```ts 47 | const ::toArray(this: Iterable): T[] { 48 | return [...this] 49 | } 50 | 51 | const ::last { 52 | get(this: T[]): T { return this[this.length - 1] } 53 | set(this: T[], v: T) { this[this.length - 1] = v } 54 | } 55 | 56 | const ::map = Array.prototype.map // infer type 57 | const ::size = Object.getOwnPropertyDescriptor(Set.prototype, 'size') as { get(this: Set): number } 58 | ``` 59 | 60 | 61 | ## Syntax ambiguity of `A::B:C` 62 | 63 | There are two rules to help to mitigate the ambiguity of using `A::B:C` with `? ... :` or `case ... :` . 64 | 65 | ### Newline is not allowed around `:` in `A::B:C` 66 | 67 | Aka. `B : C` must be in the same line. So 68 | ```js 69 | let x = foo 70 | ? A::B 71 | : C 72 | ``` 73 | and 74 | ```js 75 | case A::B: 76 | C 77 | ``` 78 | will work as developer expected. 79 | 80 | ### `A::B:C` has the higher precedence 81 | 82 | So `foo ? A :: B : C` will always be parsed as `foo ? (A::B:C)`, not `foo ? (A::B) : C`; 83 | `case A :: B : C` will always be parsed as `case (A::B:C)`, not `case (A::B): C`. 84 | 85 | Alternative 1: do not allow spaces between `:` and `C`, so `foo ? A::B : C` and `case A::B: C` will work as developer expect. 86 | 87 | Alternative 2: make `foo ? A :: B : C` or `case A :: B : C` be syntax error and force programmers use parens in such cases. 88 | 89 | --- 90 | 91 | Another alternative is replacing `:` to other character. It can't be any exist binary operator. Unary operators `!` or `~` could be reused, though it will make them not available for potential overload for binary operator. 92 | 93 | ```js 94 | x::Ext:name 95 | 96 | x::Ext~name 97 | x::Ext!name 98 | 99 | x::Ext#name 100 | x::Ext@name 101 | 102 | x::Ext'name 103 | x::Ext"name 104 | ``` 105 | -------------------------------------------------------------------------------- /docs/usecases.md: -------------------------------------------------------------------------------- 1 | # Use cases 2 | 3 | ## Borrow built-in methods 4 | 5 | ```js 6 | value::Object:toString() // Object.prototype.toString.call(value) 7 | ``` 8 | 9 | --- 10 | 11 | ```js 12 | indexed::Array:map(x => x * x) // Array.prototype.map.call(indexed, x => x * x) 13 | indexed::Array:filter(x => x > 0) // Array.prototype.filter.call(indexed, x => x > 0) 14 | ``` 15 | 16 | ```js 17 | const ::{map, filter} from Array 18 | 19 | indexed::map(x => x * x) 20 | indexed::filter(x => x > 0) 21 | ``` 22 | 23 | --- 24 | 25 | ```js 26 | const o = Object.create(null) 27 | 28 | for (const key in o) { 29 | if (o.hasOwnProperty(key)) { // throw Error 30 | //... 31 | } 32 | } 33 | ``` 34 | 35 | ```js 36 | const o = Object.create(null) 37 | 38 | for (const key in o) { 39 | if (o::Object:hasOwnProperty(key)) { // Object.prototype.hasOwnProperty.call(o, key) 40 | //... 41 | } 42 | } 43 | ``` 44 | 45 | ```js 46 | const ::{hasOwnProperty as own} from Object 47 | 48 | const o = Object.create(null) 49 | 50 | for (const key in o) { 51 | if (o::own(key)) { 52 | //... 53 | } 54 | } 55 | ``` 56 | 57 | 58 | ## Code readability 59 | 60 | ```js 61 | // WebAssembly.instantiateStreaming(fetch('simple.wasm')) 62 | fetch('simple.wasm')::WebAssembly:instantiateStreaming() 63 | ``` 64 | 65 | ```js 66 | // Math.abs(a.very.long.and.complex.expression) 67 | a.very.long.and.complex.expression::Math:abs() 68 | ``` 69 | 70 | ## CSS Unit 71 | 72 | ```js 73 | 1::CSS:px() // CSSUnitValue {value: 1, unit: "px"} 74 | ``` 75 | 76 | Note: Need small hack due to https://www.w3.org/Bugs/Public/show_bug.cgi?id=29623 77 | ```js 78 | CSS[Symbol.extension] = Extension.from({...CSS}) 79 | ``` 80 | 81 | ```js 82 | const ::px = Extension.accessor(CSS.px) 83 | 1::px // CSSUnitValue {value: 1, unit: "px"} 84 | ``` 85 | 86 | ## [First-class protocols](https://github.com/michaelficarra/proposal-first-class-protocols) 87 | 88 | ```js 89 | protocol Foldable { 90 | foldr 91 | toArray() { 92 | return this[Foldable.foldr]( 93 | (m, a) => [a].concat(m), []) 94 | } 95 | get size() { 96 | return this[Foldable.foldr](m => m + 1, 0) 97 | } 98 | contains(eq, e) { 99 | return this[Foldable.foldr]( 100 | (m, a) => m || eq(a, e), 101 | false) 102 | } 103 | } 104 | 105 | // assume iter.constructor implements Foldable 106 | iter[Foldable.toArray]() 107 | iter[Foldable.size] 108 | 109 | // better syntax 110 | iter::Foldable:toArray() 111 | iter::Foldable:size 112 | 113 | // if used frequently, could extact the extension methods/accessors 114 | const ::{toArray, size} from Foldable 115 | 116 | iter::toArray() 117 | iter::size 118 | ``` 119 | 120 | ```js 121 | iter.constructor implements Foldable // true 122 | iter[Foldable.toArray]() 123 | iter[Foldable.size] 124 | 125 | x.constructor implements Foldable // false 126 | x[Foldable.toArray] = function () { ... } 127 | x[Foldable.toArray]() // also ok 128 | x[Foldable.size] // undefined 129 | 130 | // ensure really implemented protocol 131 | const ::{toArray, size} from Foldable 132 | iter::toArray() 133 | iter::size 134 | x::toArray() // throw TypeError 135 | x::size // throw TypeError 136 | ``` 137 | 138 | ```js 139 | protocol Foldable { 140 | foldr 141 | toArray() { 142 | return this[Foldable.foldr]((m, a) => [a].concat(m), []) 143 | } 144 | get size() { 145 | return this[Foldable.foldr](m => m + 1, 0) 146 | } 147 | contains(eq, e) { 148 | return this[Foldable.foldr]((m, a) => m || eq(a, e), false) 149 | } 150 | } 151 | ``` 152 | 153 | Possible shorthand syntax 154 | 155 | ```js 156 | protocol Foldable { 157 | foldr 158 | toArray() { 159 | return this::foldr((m, a) => [a].concat(m), []) 160 | } 161 | get size() { 162 | return this::foldr(m => m + 1, 0) 163 | } 164 | contains(eq, e) { 165 | return this::foldr((m, a) => m || eq(a, e), false) 166 | } 167 | } 168 | ``` 169 | 170 | 171 | ## Extract methods 172 | 173 | ```js 174 | const f = obj.method.bind(obj) 175 | 176 | // const f = ::obj.method 177 | ``` 178 | 179 | ```js 180 | const ::extractMethod = function (methodName) { 181 | return this[methodName].bind(this) 182 | } 183 | 184 | const foo = obj::extractMethod('method') 185 | ``` 186 | 187 | ```js 188 | const bind = { 189 | [Symbol.extension]: { 190 | get(receiver, name) { 191 | return receiver[name].bind(receiver) 192 | } 193 | } 194 | } 195 | 196 | const f = obj::bind:method // bind[Symbol.extension].get(obj, 'method') 197 | ``` 198 | 199 | ```js 200 | const get = { 201 | [Symbol.extension]: { 202 | get(receiver, name) { 203 | return () => receiver[name] 204 | } 205 | } 206 | } 207 | const set = { 208 | [Symbol.extension]: { 209 | get(receiver, name) { 210 | return (v) => receiver[name] = v 211 | } 212 | } 213 | } 214 | 215 | const getter = obj::get:prop 216 | const setter = obj::set:prop 217 | ``` 218 | 219 | ## Partial application 220 | 221 | ```js 222 | const $ = Symbol('placeholder') 223 | function pcall(f, ...args) { 224 | return x => f(...args.map(arg => arg === $ ? x : arg)) 225 | } 226 | 227 | const reciprocal = pcall(div, 1, $) 228 | 229 | function div(a, b) { return a / b } 230 | ``` 231 | 232 | ```js 233 | const ::pcall = Extension.method(particalCall) 234 | 235 | const reciprocal = div::pcall(1, $) // div(1, ?) 236 | ``` 237 | 238 | ```js 239 | const pinvoke = { 240 | [Symbol.extension]: { 241 | invoke(receiver, name, args) { 242 | return x => receiver[name](...args.map(arg => arg === $ ? x : arg)) 243 | } 244 | } 245 | } 246 | 247 | obj::pinvoke:foo(1, $) // obj.foo(1, ?) 248 | ``` 249 | 250 | ## Pipe method 251 | 252 | ```js 253 | const ::pipe = function (f) { 254 | return f(this) 255 | } 256 | let result = "hello" 257 | ::pipe(doubleSay) 258 | ::pipe(capitalize) 259 | ::pipe(exclaim) 260 | ``` 261 | 262 | ## await 263 | 264 | ```js 265 | let result = await fetch("https://example.org/url").then(res => res.json()) 266 | let result = await (await fetch("https://example.org/url")).json() 267 | ``` 268 | 269 | ```js 270 | let result = "https://example.org/url" 271 | |> fetch 272 | |> await 273 | |> res => res.json() 274 | |> await 275 | ``` 276 | 277 | ```js 278 | let result = fetch("https://example.org/url")::await().json()::await() 279 | ``` 280 | 281 | ```js 282 | value::await() // possible solution of top-level await in script 283 | ``` 284 | 285 | ## throw 286 | 287 | ```js 288 | valid ? 42 : throw new TypeError() // throw expressions 289 | valid ? 42 : new TypeError()::throw() 290 | ``` 291 | 292 | ## new 293 | 294 | ```js 295 | let a = new (unsigned ? Uint8Array : Int8Array)(4) 296 | ``` 297 | 298 | ```js 299 | let a = (unsigned ? Uint8Array : Int8Array)::new(4) 300 | ``` 301 | 302 | # Pipeline 303 | ```js 304 | value 305 | |> await # 306 | |> doubleSay(#, ', ') 307 | |> capitalize // This is a function call. 308 | |> # + '!' 309 | |> new User.Message(#) 310 | |> await # 311 | |> console.log; // This is a method call. 312 | ``` 313 | 314 | ```js 315 | value 316 | ::await() 317 | ::pipe(doubleSay, $, ', ') 318 | ::pipe(capitalize) 319 | ::pipe(s => s + '!') 320 | ::pipe(User.Message) 321 | ::await() 322 | ::pipe(s => console.log(s)) 323 | ``` 324 | 325 | ```js 326 | value 327 | ::await() 328 | ::pipe(doubleSay, $, ', ') 329 | ::pipe(capitalize) 330 | ::String:concat('!') 331 | ::pipe(User.Message) 332 | ::await() 333 | ::console:log() 334 | ``` 335 | 336 | 337 | ## Sensitive code (use Branding as example) 338 | 339 | ```js 340 | // Could be hijacked by modifying `WeakSet.prototype`! 341 | 342 | const brand = new WeakSet() 343 | 344 | function installBrand(o) { 345 | brand.add(o) 346 | } 347 | 348 | function checkBrand(o) { 349 | return brand.has(o) 350 | } 351 | ``` 352 | 353 | 354 | ```js 355 | // Inconvenient syntax 356 | // Could be hijacked by modifying `Function.prototype.call`! 357 | 358 | const {has, add} = WeakSet.prototype 359 | const brand = new WeakSet() 360 | 361 | function installBrand(o) { 362 | add.call(brand, o) 363 | } 364 | 365 | function checkBrand(o) { 366 | return has.call(brand, o) 367 | } 368 | ``` 369 | 370 | ```js 371 | // Inconvenient syntax 372 | 373 | const call = Reflect.apply 374 | const {has, add} = WeakSet.prototype 375 | const brand = new WeakSet() 376 | 377 | function installBrand(o) { 378 | call(add, brand, o) 379 | } 380 | 381 | function checkBrand(o) { 382 | return call(has, brand, o) 383 | } 384 | ``` 385 | 386 | 387 | ```js 388 | // must remember adding new methods 389 | // how about accessors? 390 | 391 | const brand = new WeakSet() 392 | brand.add = WeakSet.prototype.add 393 | brand.has = WeakSet.prototype.has 394 | 395 | function installBrand(o) { 396 | brand.add(o) 397 | } 398 | 399 | function checkBrand(o) { 400 | return brand.has(o) 401 | } 402 | ``` 403 | 404 | Simple solution: 405 | 406 | ```js 407 | const ::{has, add} from WeakSet 408 | const brand = new WeakSet() 409 | 410 | function installBrand(o) { 411 | brand::add(o) 412 | } 413 | 414 | function checkBrand(o) { 415 | return brand::has(o) 416 | } 417 | ``` 418 | 419 | ## Internal slot 420 | 421 | ```js 422 | function Slot() { 423 | const store = new WeakMap() 424 | return { 425 | get() { 426 | if (!store.has(this)) throw new TypeError() 427 | return store.get(this) 428 | }, 429 | set(v) { 430 | if (!store.has(this)) throw new TypeError() 431 | store.set(this, v) 432 | }, 433 | install() { 434 | if (store.has(this)) throw new TypeError() 435 | store.set(this, undefined) 436 | }, 437 | } 438 | } 439 | 440 | function createObjectWithSlots(proto, slots) { 441 | const o = Object.create(proto) 442 | for (const slot of slots) o::slot:install() 443 | return o 444 | } 445 | 446 | const foobarSlots = [Slot(), Slot()] 447 | const ::[foo, bar] = foobarSlots 448 | const ::foobar = function () { 449 | return this::foo + this::bar 450 | } 451 | 452 | let o = createObjectWithSlots({}, foobarSlots) 453 | o::foo = 40 454 | o::bar = 2 455 | console.log(o::foo, o::bar, o::foobar()) // 40, 2, 42 456 | ``` 457 | 458 | Symbol group 459 | 460 | ```js 461 | const A = SymbolGroup() 462 | const ::{a1, a2} = A.accessors() 463 | const B = SymbolGroup() 464 | const ::{b1} = B.accessors() 465 | 466 | class Test { 467 | constructor(foo, bar) { 468 | this::A:init({foo, bar}) 469 | } 470 | foobar() { 471 | return this::foo + this::bar 472 | } 473 | } 474 | 475 | const x = new Test(1, 2) 476 | x.foobar() // 3 477 | 478 | x::foo = 40 479 | x.foobar() // 42 480 | 481 | const y = {} 482 | y::foo = 1 // throw 483 | ``` 484 | 485 | Property access 486 | 487 | ```js 488 | const key = new Extension({ 489 | get(receiver, key) { 490 | if (!(key in receiver)) throw new TypeError() 491 | return receiver[key] 492 | }, 493 | set(receiver, key, value) { 494 | if (!(key in receiver)) throw new TypeError() 495 | receiver[key] = value 496 | }, 497 | }) 498 | 499 | const o = {x: 1, y: 2} 500 | o::key:x // 1 501 | ++o::key:y // 3 502 | o::key:z // throw TypeError 503 | ``` 504 | 505 | 506 | 507 | ## Eventual Send 508 | 509 | ```js 510 | // https://github.com/tc39/proposal-eventual-send 511 | 512 | // E and E.sendOnly convenience proxies 513 | import { E } from 'js:eventual-send'; 514 | 515 | // Invoke pipelined RPCs. 516 | const fileP = E( 517 | E(target).openDirectory(dirName) 518 | ).openFile(fileName); 519 | // Process the read results after a round trip. 520 | E(fileP).read().then(contents => { 521 | console.log('file contents', contents); 522 | // We don't use the result of this send. 523 | E.sendOnly(fileP).append('fire-and-forget'); 524 | }); 525 | ``` 526 | 527 | ```js 528 | // Invoke pipelined RPCs. 529 | const fileP = HandledPromise.applyMethod( 530 | HandledPromise.applyMethod(target, 'openDirectory', [dirName]), 531 | 'openFile', [filename]) 532 | // Process the read results after a round trip. 533 | HandledPromise.applyMethod(fileP, 'read', []).then(contents => { 534 | console.log('file contents', contents); 535 | // We don't use the result of this send. 536 | HandledPromise.applyMethodSendOnly(fileP, 'append', ['fire-and-forget']) 537 | }); 538 | ``` 539 | 540 | - need better syntax 541 | - need two type of Proxy to differentiate `get` and `applyMethod` 542 | 543 | ```js 544 | // https://github.com/tc39/proposal-wavy-dot 545 | const fileP = target 546 | ~.openDirectory(dirName) 547 | ~.openFile(fileName) 548 | // Process the read results after a round trip. 549 | fileP~.read().then(contents => { 550 | console.log('file contents', contents) 551 | // We don't use the result of this send. 552 | fileP~.append('fire-and-forget') 553 | }) 554 | ``` 555 | 556 | ```js 557 | const send = new Extension({ 558 | apply(receiver, key, args) { 559 | return HandledPromise.applyMethod(receiver, key, args) 560 | }, 561 | get(receiver, key) { 562 | return HandledPromise.get(receiver, key) 563 | }, 564 | set(receiver, key, value) { 565 | return HandledPromise.set(receiver, key, value) 566 | }, 567 | }) 568 | const sendOnly = new Extension({ 569 | apply(key, args) { 570 | return HandledPromise.applyMethodSendOnly(this, key, args) 571 | }, 572 | get(key) { 573 | return HandledPromise.getSendOnly(this, key) 574 | }, 575 | set(key, value) { 576 | return HandledPromise.setSendOnly(this, key, value) 577 | }, 578 | }) 579 | 580 | // Invoke pipelined RPCs. 581 | const fileP = target 582 | ::send:openDirectory(dirName) 583 | ::send:openFile(fileName) 584 | // Process the read results after a round trip. 585 | fileP::send:read().then(contents => { 586 | console.log('file contents', contents) 587 | // We don't use the result of this send. 588 | fileP::sendOnly:append('fire-and-forget') 589 | }) 590 | ``` 591 | 592 | - Not as concise as special operator, but still elegant 593 | - Save the syntax space 594 | -------------------------------------------------------------------------------- /experimental/Extension.js: -------------------------------------------------------------------------------- 1 | import { Call, ToObject, IsCallable, DefinePropertyOrThrow, ToLength } from './common/abstract.js' 2 | import { ObjectCreate, OwnPropertyStringKeys, ThrowTypeError, IsClassConstructor } from './common/extra.js' 3 | import InternalSlot from './common/InternalSlot.js' 4 | 5 | import { SymbolExtension } from './ternary.js' 6 | import { CreateExtMethod, CallExtMethod, CallExtGetter, CallExtSetter } from './binary.js' 7 | 8 | const ExtensionMethods = InternalSlot() 9 | 10 | function ExtensionGetMethod(extension, name) { 11 | const methods = ExtensionMethods.get(extension) 12 | const m = methods[name] 13 | if (m === undefined) ThrowTypeError() 14 | return m 15 | } 16 | 17 | export default class Extension { 18 | constructor(extensionMethods) { 19 | extensionMethods = ToObject(extensionMethods) 20 | const methods = ObjectCreate(null) 21 | for (const name of OwnPropertyStringKeys(extensionMethods)) { 22 | methods[name] = CreateExtMethod(extensionMethods[name]) 23 | } 24 | ExtensionMethods.install(this, methods) 25 | } 26 | 27 | get [SymbolExtension]() { return this } 28 | 29 | invoke(receiver, name, args, O) { 30 | const m = ExtensionGetMethod(this, name) 31 | return CallExtMethod(m, receiver, args) 32 | } 33 | get(receiver, name, O) { 34 | const m = ExtensionGetMethod(this, name) 35 | return CallExtGetter(m, receiver) 36 | } 37 | set(receiver, name, V, O) { 38 | const m = ExtensionGetMethod(this, name) 39 | return CallExtSetter(m, receiver, V) 40 | } 41 | 42 | static from(source) { 43 | const ext = source[SymbolExtension] 44 | if (ext !== undefined) return ToObject(ext) 45 | if (IsConstructor(source)) return new Extension(CollectMethods(source.prototype, true), option) 46 | // for first-class protocol proposal 47 | // if (IsProtocol(source)) return new Extension(CreateMethodsFromProtocol(source), option) 48 | return new Extension(CollectMethods(source, false), option) 49 | } 50 | 51 | static method(value, options = undefined) { 52 | return ToMethod('invoke', value, options) 53 | } 54 | static accessor(get, set, arg3 = undefined, arg4 = undefined) { 55 | let getOptions, setOptions 56 | if (IsCallable(set)) { 57 | setOptions = arg3 58 | } else { 59 | getOptions = set 60 | set = arg3 61 | setOptions = arg4 62 | } 63 | const result = {} 64 | if (get !== undefined && get !== null) result.get = ToMethod('get', get, getOptions) 65 | if (set !== undefined && set !== null) result.set = ToMethod('set', set, setOptions) 66 | return result 67 | } 68 | } 69 | 70 | function CollectMethods(O, isProto) { 71 | const names = OwnPropertyStringKeys(O) 72 | for (const k of names) { 73 | const v = isProto 74 | ? ExtMethodFromPropDesc(GetOwnPropertyDescriptor(O, k)) 75 | : ExtMethodFromMethod(O, k) 76 | if (v !== undefined) extension[k] = v 77 | } 78 | return extension 79 | } 80 | 81 | function ExtMethodFromPropDesc(pd) { 82 | if (pd === undefined) return undefined 83 | const {value, get, set} = pd 84 | if (get || set) return {get, set} 85 | if (IsCallable(value)) return value 86 | return undefined 87 | } 88 | 89 | function ExtMethodFromMethod(o, k) { 90 | const v = o[k] 91 | if (IsCallable(v) && !IsClassConstructor(v)) { 92 | return ToMethod('invoke', BindThisArg(v, o)) 93 | } 94 | return undefined 95 | } 96 | 97 | function ToMethod(type, V, options) { 98 | if (!IsCallable(V)) throw new TypeError() 99 | if (IsClassConstructor(V)) throw new TypeError() 100 | 101 | options ??= {} 102 | const {receiver} = options 103 | let first = false, last = false, spread = false 104 | if (receiver) { 105 | for (const token of String(receiver).trim().split(/\s+/)) { 106 | switch (token) { 107 | case 'spread': spread = true; break 108 | case 'last': last = true; break 109 | case 'first': first = true; break 110 | default: throw new TypeError() 111 | } 112 | } 113 | if (first && last) throw new TypeError() 114 | } 115 | 116 | const f = spread 117 | ? last 118 | ? function (...args) { return V(...args, ...this) } 119 | : function (...args) { return V(...this, ...args) } 120 | : last 121 | ? function (...args) { return V(...args, this) } 122 | : function (...args) { return V(this, ...args) } 123 | 124 | DefinePropertyOrThrow(f, 'name', {value: `${type} ${V.name}`}) 125 | DefinePropertyOrThrow(f, 'length', {value: ToLength(V.length - 1)}) 126 | return f 127 | } 128 | 129 | export function BindThisArg(F, O) { 130 | if (IsBoundFunction(F) || IsArrowFunction(F)) return F 131 | return Call(FunctionPrototypeBind, F, [O]) 132 | } 133 | const FunctionPrototypeBind = Function.prototype.bind 134 | -------------------------------------------------------------------------------- /experimental/binary.js: -------------------------------------------------------------------------------- 1 | import {Call, ToObject, IsCallable} from './common/abstract.js' 2 | import {ThrowTypeError, IsClassConstructor, IsBoundFunction, IsArrowFunction} from './common/extra.js' 3 | 4 | // x::method(...args) -> CallExtMethod($method, x, args) 5 | export function CallExtMethod(extensionMethod, thisArgument, argumentsList) { 6 | return Call(extensionMethod.invoke, thisArgument, argumentsList) 7 | } 8 | 9 | // x::prop -> CallExtGetter($prop, x) 10 | export function CallExtGetter(extensionAccessor, thisArgument) { 11 | return Call(extensionAccessor.get, thisArgument, []) 12 | } 13 | 14 | // x::prop = value -> CallExtSetter($prop, x, value) 15 | export function CallExtSetter(extensionAccessor, thisArgument, V) { 16 | Call(extensionAccessor.set, thisArgument, [V]) 17 | return V 18 | } 19 | 20 | // const ::method = o -> $method = CreateExtMethod(o) 21 | export function CreateExtMethod(O) { 22 | O = ToObject(O) 23 | if (IsCallable(O)) { 24 | CheckMethod(O) 25 | return {invoke: O} 26 | } 27 | const {get, set, value} = O 28 | if (get !== undefined || set !== undefined) { 29 | if (get !== undefined) CheckMethod(get) 30 | if (set !== undefined) CheckMethod(set) 31 | return { 32 | get, set, 33 | invoke(...args) { 34 | const f = Call(get, this) 35 | return Call(f, this, args) 36 | }, 37 | } 38 | } 39 | CheckMethod(value) 40 | return {invoke: value} 41 | } 42 | 43 | function CheckMethod(F) { 44 | if (!IsCallable(F)) ThrowTypeError() 45 | if (IsArrowFunction(F) || IsBoundFunction(F)) ThrowTypeError() 46 | if (IsClassConstructor(F)) ThrowTypeError() 47 | // Optional: throw type error for other non-methods (functions ignore receiver) 48 | } 49 | -------------------------------------------------------------------------------- /experimental/common/InternalSlot.js: -------------------------------------------------------------------------------- 1 | import { Call } from './abstract.js' 2 | import { ThrowTypeError } from './extra.js' 3 | 4 | const {WeakMap} = globalThis 5 | const {has, get, set} = WeakMap.prototype 6 | 7 | export default function InternalSlot() { 8 | const wm = new WeakMap() 9 | const wm_has = k => Call(has, wm, [k]) 10 | const wm_get = k => Call(get, wm, [k]) 11 | const wm_set = (k, v) => Call(set, wm, [k, v]) 12 | return { 13 | install(object, value) { 14 | Assert(!wm_has(object)) 15 | wm_set(object, value) 16 | }, 17 | has(object) { 18 | return wm_has(object) 19 | }, 20 | get(object) { 21 | if (!wm_has(object)) ThrowTypeError() 22 | return wm_get(object) 23 | }, 24 | set(object, value) { 25 | if (!wm_has(object)) ThrowTypeError() 26 | wm_set(object, value) 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /experimental/common/abstract.js: -------------------------------------------------------------------------------- 1 | import {Assert, GetOwnPropertyDescriptor} from './extra.js' 2 | const {Math, TypeError, Object, Number, String, Reflect} = globalThis 3 | 4 | // 7.1 Type Conversion 5 | 6 | // 7.1.5 7 | export const ToIntegerOrInfinity = Math.floor 8 | 9 | // 7.1.18 10 | export function ToObject(argument) { 11 | if (argument === undefined || argument === null) throw new TypeError() 12 | return Object(argument) 13 | } 14 | 15 | // 7.1.20 16 | export function ToLength(argument) { 17 | const n = ToIntegerOrInfinity(argument) 18 | if (n <= 0) return 0 19 | if (n >= MAX_SAFE_INTEGER) return MAX_SAFE_INTEGER 20 | return n 21 | } 22 | const {MAX_SAFE_INTEGER} = Number 23 | 24 | // 7.2 Testing and Comparison Operations 25 | 26 | // 7.2.1 27 | export function RequireObjectCoercible(argument) { 28 | ToObject(argument) 29 | return argument 30 | } 31 | 32 | // 7.2.3 33 | export function IsCallable(value) { 34 | return typeof value === 'function' 35 | } 36 | 37 | // 7.2.4 38 | export function IsConstructor(value) { 39 | return IsCallable(value) && HasOwnProperty(value, 'prototype') 40 | } 41 | 42 | // 7.2.9 43 | export function IsStringPrefix(p, q) { 44 | Assert(typeof p === 'string') 45 | Assert(typeof q === 'string') 46 | return Call(StringPrototypeStartsWith, q, [p]) 47 | } 48 | const StringPrototypeStartsWith = String.prototype.startsWith 49 | 50 | // 7.3 Operations on Objects 51 | 52 | // 7.3.8 53 | export const DefinePropertyOrThrow = Reflect.defineProperty 54 | 55 | // 7.3.10 56 | export function GetMethod(V, P) { 57 | const func = V[P] 58 | if (func === undefined || func === null) return undefined 59 | if (!IsCallable(func)) throw new TypeError() 60 | return func 61 | } 62 | 63 | // 7.3.12 64 | export function HasOwnProperty(object, key) { 65 | return GetOwnPropertyDescriptor(object, key) !== undefined 66 | } 67 | 68 | // 7.3.13 69 | export function Call(F, V, argumentsList = []) { 70 | return Apply(F, V, argumentsList) 71 | } 72 | const Apply = Reflect.apply 73 | 74 | // 7.3.14 75 | export const Construct = Reflect.construct 76 | -------------------------------------------------------------------------------- /experimental/common/extra.js: -------------------------------------------------------------------------------- 1 | const {Object, Function, Reflect, TypeError} = globalThis 2 | 3 | // 5.2 Algorithm Conventions 4 | 5 | export function Assert(invariant) { 6 | if (!invariant) throw 'Assertion failed' 7 | } 8 | 9 | // 6.1 ECMAScript Language Types 10 | 11 | export function IsObject(x) { 12 | return Object(x) === Object(x) 13 | } 14 | 15 | export function IsString(x) { 16 | return typeof x === 'string' 17 | } 18 | 19 | 20 | import {IsCallable, IsConstructor, IsStringPrefix, Call} from './abstract.js' 21 | 22 | export function IsClassConstructor(value) { 23 | return IsConstructor(value) && 24 | /^class\b/.test(FunctionSource(value)) // value.[[IsClassConstructor]] 25 | } 26 | 27 | export function IsBoundFunction(value) { 28 | return IsCallable(value) && 29 | IsStringPrefix('bound ', value.name) // value.[[BoundTargetFunction] 30 | } 31 | 32 | export function IsArrowFunction(value) { 33 | if (!IsCallable(value)) return false 34 | // return value.[[ThisMode]] === 'lexical' 35 | if (IsConstructor(value)) return false 36 | const s = FunctionSource(value) 37 | if (IsStringPrefix('(', s)) return true 38 | if (/\s/.test(value.name)) return false 39 | if (/^(?:function|class)\b/.test(s)) return false 40 | return /^[\w$]+\s*=>/.test(s) 41 | } 42 | 43 | function FunctionSource(func) { 44 | return Call(FunctionPrototypeToString, func, []) 45 | } 46 | const FunctionPrototypeToString = Function.prototype.toString 47 | 48 | // O.[[GetOwnProperty]](P) 49 | export const GetOwnPropertyDescriptor = Reflect.getOwnPropertyDescriptor 50 | 51 | // O.[[OwnPropertyKeys]]() 52 | export const OwnPropertyKeys = Reflect.ownKeys 53 | 54 | export const OwnPropertyStringKeys = Object.getOwnPropertyNames 55 | 56 | export const OwnPropertySymbolKeys = Object.getOwnPropertySymbols 57 | 58 | export function WellKnownSymbol(s) { 59 | return Symbol.for(`Symbol.{s}`) 60 | } 61 | 62 | export function ThrowTypeError() { 63 | throw new TypeError() 64 | } 65 | 66 | export const ObjectCreate = Object.create 67 | -------------------------------------------------------------------------------- /experimental/index.js: -------------------------------------------------------------------------------- 1 | export * from './binary.js' 2 | export * from './ternary.js' 3 | export {default as Extension} from './Extension.js' 4 | -------------------------------------------------------------------------------- /experimental/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "test": "node test" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /experimental/ternary.js: -------------------------------------------------------------------------------- 1 | import {Call, ToObject, IsConstructor, HasOwnProperty} from './common/abstract.js' 2 | import {WellKnownSymbol, GetOwnPropertyDescriptor, ThrowTypeError} from './common/extra.js' 3 | 4 | // x::ext:name(...args) -> ExtInvoke(ext, 'name', x, args) 5 | export function ExtInvoke(O, name, thisArgument, argumentsList) { 6 | const ext = GetExtension(O) 7 | const {invoke} = ext 8 | if (invoke !== undefined) { 9 | return Call(invoke, ext, [thisArgument, name, argumentsList, O]) 10 | } 11 | const f = ext.get(thisArgument, name, O) 12 | return Call(f, thisArgument, argumentsList) 13 | } 14 | 15 | // x::ext:name -> ExtGet(ext, 'name', x, []) 16 | export function ExtGet(O, name, thisArgument) { 17 | const ext = GetExtension(O) 18 | return ext.get(thisArgument, name, O) 19 | } 20 | 21 | // x::ext:name = value -> ExtSet(ext, 'name', x, value) 22 | export function ExtSet(O, name, thisArgument, V) { 23 | const ext = GetExtension(O) 24 | ext.set(thisArgument, name, V, O) 25 | return V 26 | } 27 | 28 | export const SymbolExtension = WellKnownSymbol('extension') 29 | 30 | function GetExtension(O) { 31 | const ext = O[SymbolExtension] 32 | if (ext !== undefined) return ToObject(ext) 33 | if (IsConstructor(O)) { 34 | ToObject(O.prototype) 35 | return ConstructorExtension 36 | } 37 | // for first-class protocol proposal 38 | // if (IsProtocol(value)) return ProtocolExtension 39 | return NamespaceExtension 40 | } 41 | 42 | const ConstructorExtension = { 43 | get [SymbolExtension]() { return this }, 44 | invoke(receiver, name, args, C) { 45 | const {value, get} = GetOwnPropertyDescriptor(C.prototype, name) 46 | if (value !== undefined) return Call(value, receiver, args) 47 | const f = Call(get, receiver) 48 | return Call(f, receiver, args) 49 | }, 50 | get(receiver, name, C) { 51 | const {get} = GetOwnPropertyDescriptor(C.prototype, name) 52 | return Call(get, receiver, []) 53 | }, 54 | set(receiver, name, V, C) { 55 | const {set} = GetOwnPropertyDescriptor(C.prototype, name) 56 | return Call(set, receiver, V) 57 | }, 58 | } 59 | 60 | const NamespaceExtension = { 61 | get [SymbolExtension]() { return this }, 62 | invoke(receiver, name, args, O) { 63 | if (!HasOwnProperty(O, name)) ThrowTypeError() 64 | return O[name](receiver, ...args) 65 | }, 66 | } 67 | -------------------------------------------------------------------------------- /experimental/test/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Extension, 3 | ExtInvoke, ExtGet, 4 | CreateExtMethod, CallExtMethod, CallExtGetter, CallExtSetter 5 | } from '../index.js' 6 | 7 | const {assert, log} = console 8 | 9 | { 10 | const x = {length: 2, 0: 'a', 1: 'b'} 11 | // const s = x::Array:join('/') 12 | const s = ExtInvoke(Array, 'join', x, ['/']) 13 | assert(s === 'a/b') 14 | } 15 | { 16 | const x = new Set(['a', 'b']) 17 | // const n = x::Set:size 18 | const n = ExtGet(Set, 'size', x) 19 | assert(n === 2) 20 | } 21 | { 22 | // const *::toHex = function () { return this.toString(16) } 23 | const $toHex = CreateExtMethod(function () { return this.toString(16) }) 24 | // const hex = 255::toHex() 25 | const s = CallExtMethod($toHex, 255, []) 26 | assert(s === 'ff') 27 | } 28 | { 29 | // const *::hex = Extension.accessor(n => n.toString(16)) 30 | const $hex = CreateExtMethod(Extension.accessor(n => n.toString(16))) 31 | // const hex = 255::hex 32 | const s = CallExtGetter($hex, 255) 33 | assert(s === 'ff') 34 | } 35 | { 36 | const who = {firstName: 'Shijun', lastName: 'He'} 37 | // const *::fullName = { ... } 38 | const $fullName = CreateExtMethod({ 39 | get() { 40 | return this.firstName + ' ' + this.lastName 41 | }, 42 | set(value) { 43 | [this.firstName, this.lastName] = value.split(' ') 44 | } 45 | }) 46 | // const s = who::fullName 47 | const s = CallExtGetter($fullName, who) 48 | assert(s === 'Shijun He') 49 | // who::fullName = 'Shi-Jun HE' 50 | CallExtSetter($fullName, who, 'Shi-Jun HE') 51 | assert(who.lastName === 'HE') 52 | } 53 | 54 | log('tests ok.') 55 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Proposal Title Goes Here

Stage -1 Draft / September 25, 2017

Proposal Title Goes Here

1796 | 1797 | 1798 |

1This is an emu-clause

1799 |

This is an algorithm:

1800 |
  1. Let proposal be undefined.
  2. If IsAccepted(proposal),
    1. Let stage be 0.
  3. Else,
    1. Let stage be -1.
  4. Return ? ToString(proposal). 1801 |
1802 |
1803 |

ACopyright & Software License

1804 | 1805 |

Copyright Notice

1806 |

© 2017 Your Name Goes Here

1807 | 1808 |

Software License

1809 |

All Software contained in this document ("Software") is protected by copyright and is being made available under the "BSD License", included below. This Software may be subject to third party rights (rights from parties other than Ecma International), including patent rights, and no licenses under such third party rights are granted under this license even if the third party concerned is a member of Ecma International. SEE THE ECMA CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT http://www.ecma-international.org/memento/codeofconduct.htm FOR INFORMATION REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA INTERNATIONAL STANDARDS.

1810 | 1811 |

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1812 | 1813 |
    1814 |
  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. 1815 |
  3. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  4. 1816 |
  5. Neither the name of the authors nor Ecma International may be used to endorse or promote products derived from this software without specific prior written permission.
  6. 1817 |
1818 | 1819 |

THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

1820 | 1821 |
1822 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "template-for-proposals", 4 | "description": "A repository template for ECMAScript proposals.", 5 | "scripts": { 6 | "build": "ecmarkup spec.emu index.html" 7 | }, 8 | "homepage": "https://github.com/tc39/template-for-proposals#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/tc39/template-for-proposals.git" 12 | }, 13 | "license": "MIT", 14 | "devDependencies": { 15 | "ecmarkup": "^3.11.5" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /spec.emu: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 |

This is an emu-clause

14 |

This is an algorithm:

15 | 16 | 1. Let _proposal_ be *undefined*. 17 | 1. If IsAccepted(_proposal_), 18 | 1. Let _stage_ be *0*. 19 | 1. Else, 20 | 1. Let _stage_ be *-1*. 21 | 1. Return ? ToString(_proposal_). 22 | 23 |
24 | --------------------------------------------------------------------------------