{"action":"created","issue":{"url":"https://api.github.com/repos/scala-js/scala-js/issues/2934","repository_url":"https://api.github.com/repos/scala-js/scala-js","labels_url":"https://api.github.com/repos/scala-js/scala-js/issues/2934/labels{/name}","comments_url":"https://api.github.com/repos/scala-js/scala-js/issues/2934/comments","events_url":"https://api.github.com/repos/scala-js/scala-js/issues/2934/events","html_url":"https://github.com/scala-js/scala-js/pull/2934","id":226758620,"number":2934,"title":"Fix #2800: Refer to the global scope instead of the global object.","user":{"login":"sjrd","id":535934,"avatar_url":"https://avatars0.githubusercontent.com/u/535934?v=3","gravatar_id":"","url":"https://api.github.com/users/sjrd","html_url":"https://github.com/sjrd","followers_url":"https://api.github.com/users/sjrd/followers","following_url":"https://api.github.com/users/sjrd/following{/other_user}","gists_url":"https://api.github.com/users/sjrd/gists{/gist_id}","starred_url":"https://api.github.com/users/sjrd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sjrd/subscriptions","organizations_url":"https://api.github.com/users/sjrd/orgs","repos_url":"https://api.github.com/users/sjrd/repos","events_url":"https://api.github.com/users/sjrd/events{/privacy}","received_events_url":"https://api.github.com/users/sjrd/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":13,"created_at":"2017-05-06T10:23:25Z","updated_at":"2017-06-07T12:12:39Z","closed_at":null,"pull_request":{"url":"https://api.github.com/repos/scala-js/scala-js/pulls/2934","html_url":"https://github.com/scala-js/scala-js/pull/2934","diff_url":"https://github.com/scala-js/scala-js/pull/2934.diff","patch_url":"https://github.com/scala-js/scala-js/pull/2934.patch"},"body":"This is a major redesign of accesses to \"global\" variables, both\r\nfrom a spec point of view and in the implementation.\r\n\r\nPreviously, Scala.js specified accesses to global variables in\r\nterms of the *global object*. It assumed that it was capable of\r\naccurately detecting the global object of any JS environment, and\r\nworked from there. For example, `js.Dynamic.global.foo` referred\r\nto `$g.foo`, where `$g` was supposed to be the global object.\r\n\r\nFrom a spec point of view, this was therefore different than how\r\na typical JavaScript code would be written. Indeed, in \"normal\" JS\r\nwe would just write `foo`, and let it be evaluated as a binding in\r\nthe *global scope*. This was however fine as, in ECMAScript 5.1,\r\nthe global scope and the global object were basically the same\r\nthing.\r\n\r\nHowever, ECMAScript 2015 changed the situation. There are now\r\nbindings in the global scope that are *not* properties of the\r\nglobal object. Those are `let`, `const` and `class` bindings at\r\nthe top-level of scripts. This means that the previous spec of\r\nScala.js was utterly incapable of referencing those. They were\r\ntotally invisible to Scala.js.\r\n\r\nHence, in this commit we completely change the spec of Scala.js,\r\nand consequently the implementation.\r\n\r\nLanguage changes\r\n================\r\n\r\nA reference such as `js.Dynamic.global.foo` is therefore specified\r\nas referring to the binding `foo` in the *global scope* of JS. Note\r\nthat it is safer than just saying \"it is equivalent to writing\r\n`foo` verbatim in JS\", as the latter could collide with local\r\nbindings in the scope chain. The spec protects references to the\r\nglobal scope from any local definition. Objects annotated with\r\n`@JSGlobalScope` are specified similarly to `js.Dynamic.global`.\r\n\r\nAlthough this trivially solves the problem of accessing `let`,\r\n`const` and `class` bindings, this spec change comes with some\r\nbackward source incompatible changes.\r\n\r\nCompile-time validation\r\n-----------------------\r\n\r\nFirst, only static bindings (known at compile-time) to variables\r\nwhose names are valid JS identifiers are allowed. For example, the\r\nfollowing are legal:\r\n\r\n    js.Dynamic.global.foo\r\n    js.Dynamic.global.selectDynamic(\"bar\")\r\n\r\nbut those are illegal:\r\n\r\n    js.Dynamic.global.`not-a-JS-identifier`\r\n    js.Dynamic.global.selectDynamic(computedName)\r\n\r\nThis restriction directly comes from the fact that only statically\r\nknown bindings with valid identifier names can be looked up in the\r\nglobal scope, even in JavaScript.\r\n\r\nThe particular identifier `arguments` is similarly forbidden, as\r\nit is a magic variable that automatically lexically binds to the\r\nmost nested enclosing function. It cannot be looked up in the\r\nglobal scope in JavaScript either.\r\n\r\nNote that `eval` is allowed, but will systematically be protected\r\nas `(0, eval)` when it is *called*, so as to enforce that the\r\nexecution of the script is done at the global level, and does not\r\nsee (compiler-generated) local bindings.\r\n\r\nThe second restriction is that references to the global scope,\r\neither as `js.Dynamic.global` or as `GlobalFoo` where `GlobalFoo`\r\nis annotated with `@JSGlobalScope`, cannot be loaded as a *value*.\r\nFor example, the following is illegal:\r\n\r\n    val x = js.Dynamic.global\r\n\r\nThis follows from the previous restriction, as references to the\r\nmembers of such a reference would have to be looked up dynamically,\r\nand that is not possible with the global scope. More generally, it\r\nmakes sense since the \"global scope\" is not a *value* either in\r\nJavaScript, contrary to the global object.\r\n\r\nRun-time semantic changes\r\n-------------------------\r\n\r\nIn addition to the above compile-time restrictions, the spec change\r\nincurs some differences in run-time semantics, when accessing a\r\nglobal variable that was not declared. For example:\r\n\r\n    val x = js.Dynamic.global.doesNotExist\r\n\r\nwould previously return `undefined`, but now throws a\r\n`ReferenceError`. Similarly, assignments to non-existent global\r\nvariables throw a `ReferenceError`, rather than creating the\r\nvariable.\r\n\r\nThis run-time semantic change is the largest source of actual\r\nbreakages in user code, as it invalidates the previous idiom used\r\nto perform feature detection in Scala.js. For example:\r\n\r\n    if (js.isUndefined(js.Dynamic.global.Promise))\r\n\r\nwould previously return `true` when `Promise` was not declared,\r\nbut will now throw a `ReferenceError`. The fix is to use\r\n`js.typeOf` instead:\r\n\r\n    if (js.typeOf(js.Dynamic.global.Promise) == \"undefined\")\r\n\r\nThis brings us to the last semantic change, which is that\r\n`js.typeOf` cannot be understood as a *method* anymore, as it does\r\nnot follow the evaluation model of Scala, which guarantees that\r\n`method(expr)` is always equivalent to\r\n`{ val x = expr; method(x) }`. With `js.typeOf(expr)`, if `expr`\r\nis a reference to a non-existent global variable, `\"undefined\"` is\r\nreturned, whereas the rewriting throws a `ReferenceError`.\r\n\r\nTo allow user code to perform their own global object detection,\r\nshould they need to do so, we make the global `this` value\r\navailable through `LinkingInfo.globalThis`.\r\n\r\n---\r\n\r\nImplementation changes\r\n======================\r\n\r\nThe above changes mostly impact the compiler and the emitter. The\r\nother steps of the pipeline are basically kept intact.\r\n\r\nThe IR is augmented with an IR node `JSGlobalRef(ident)` which\r\ndenotes a reference to a global variable, in the global scope. It\r\nis a valid lhs for the `Assign` node.\r\n\r\nThe compiler implements the compile-time validation described\r\nabove, and emits `JSGlobalRef`s instead of references to\r\n`<linkingInfo>.envInfo.global`. Other than that, nothing changes.\r\n\r\nThe emitter had to be completely overhauled. The presence of\r\n`JSGlobalRef`s caused significant issues with scoping and naming.\r\n\r\nAs a naive approach, the translation of `JSGlobalRef(Ident(\"Foo\"))`\r\nis straightforwardly `js.VarRef(js.Ident(\"Foo\"))`. However, this\r\ncan cause collisions with two sources of compiler-generated\r\nidentifiers:\r\n\r\n* Local bindings, such as `VarDef`s and `ParamDef`s.\r\n* Global encoding bindings, such as `$c_sci_List` or\r\n  `$doubleToInt`.\r\n\r\nThe first group can be dealt with locally in `JSDesugar`. If there\r\nis a reference to a `JSGlobalRef(Ident(\"Foo\"))` in the method, then\r\na local identifier `Foo` must be renamed. `JSDesugar` proceeds in\r\ntwo passes to do so, with a mechanism that stops after the first\r\npass if possible. In the entire test-suite, the second pass is\r\nnecessary for only a single method.\r\n\r\nThe second group is significantly more complicated to address. If\r\n*anywhere in the program*, someone references `$c_sci_List`, then\r\n*everywhere in the program* we need to rename our internal encoding\r\nof `$c_sci_List` into something else (we use `$$c_sci_List`, adding\r\nas many `$`s as necessary to get a non-colliding name).\r\n\r\nTo do so, `JSDesugar` and `ClassEmitter` keep track of all global\r\nvariables that are referenced anywhere, wrapped in `WithGlobals`,\r\na monadic data structure that eases that propagation. The `Emitter`\r\ninvalidates all its caches and re-emits the entire program if the\r\nset of global variables changes in an incremental run, wrt. to the\r\nprevious run. In batch-mode, there can therefore be two passes.\r\n\r\nTo reduce the likelihood of this happening, we eagerly filter out\r\nnon \"dangerous global refs\". A dangerous global ref is one that\r\nstarts with a `$` (and is not the single-char string `\"$\"`).\r\nIndeed, only \"dangerous global refs\" can collide with our internal\r\nglobal encodings. In the test-suite, there is not a single\r\nreference to a dangerous global ref, which means that the emitter\r\ncan stop after a single pass. We expect most reasonable codebases\r\nto exhibit the same property.\r\n\r\nThe test for dangerous global refs is therefore put in the\r\ntest-suite-ex, so that we do not unnecessarily slow down the\r\nemission of the normal test-suite."},"comment":{"url":"https://api.github.com/repos/scala-js/scala-js/issues/comments/306776055","html_url":"https://github.com/scala-js/scala-js/pull/2934#issuecomment-306776055","issue_url":"https://api.github.com/repos/scala-js/scala-js/issues/2934","id":306776055,"user":{"login":"sjrd","id":535934,"avatar_url":"https://avatars0.githubusercontent.com/u/535934?v=3","gravatar_id":"","url":"https://api.github.com/users/sjrd","html_url":"https://github.com/sjrd","followers_url":"https://api.github.com/users/sjrd/followers","following_url":"https://api.github.com/users/sjrd/following{/other_user}","gists_url":"https://api.github.com/users/sjrd/gists{/gist_id}","starred_url":"https://api.github.com/users/sjrd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sjrd/subscriptions","organizations_url":"https://api.github.com/users/sjrd/orgs","repos_url":"https://api.github.com/users/sjrd/repos","events_url":"https://api.github.com/users/sjrd/events{/privacy}","received_events_url":"https://api.github.com/users/sjrd/received_events","type":"User","site_admin":false},"created_at":"2017-06-07T12:12:39Z","updated_at":"2017-06-07T12:12:39Z","body":"Done. Phew, that wasn't easy ^^"}}
{"actor":{"display_login":"sjrd"}}