microsoft / microsoft/TypeScript
Reduce overhead and indirection in enum and namespace code generation
Nessuno ha ancora preso questa issue.
- Lingua principale
- Go
- Stelle
- 111k
- Fork
- 14.4k
- Merge medio
- 1g 19h
- PR unite (30g)
- 117
Descrizione
Suggestion
🔍 Search Terms
enum emit
✅ Viability Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
⭐ Suggestion
Note: this is a partial dupe of https://github.com/microsoft/TypeScript/issues/27604 but has a few key differences:
- It proposes a non-breaking change instead.
- It extends the change to namespaces as well.
- It in theory isn't tree-shakeable, but can still be optimized out by optimizers.
Suppose we have this code:
namespace Foo {
export function bar() { return 1 }
}
enum Foo {
One,
Two,
Three,
}
Currently, this results in the following emit:
"use strict";
var Foo;
(function (Foo) {
function bar() { return 1; }
Foo.bar = bar;
})(Foo || (Foo = {}));
(function (Foo) {
Foo[Foo["One"] = 0] = "One";
Foo[Foo["Two"] = 1] = "Two";
Foo[Foo["Three"] = 2] = "Three";
})(Foo || (Foo = {}));
A much better emit would be this (comments explaining each bit):
"use strict";
// Forward declare the namespace
// In modules and functions, this should just be `var Foo = {}`
var Foo = Foo || {};
// `namespace Foo {`
// Deduplicate like you already do with `let`/`const` when targeting ES5
function bar() { return 1; }
Foo.bar = bar;
// `}`
// `enum Foo {`
// ` One,`
Foo.One = 0;
Foo[0] = "One";
// ` Two,`
Foo.Two = 1;
Foo[1] = "Two";
// ` Three,`
Foo.Three = 2;
Foo[2] = "Three";
// `}`
This would carry the same observable semantics it currently does (complete with Object.prototype observability).
You may be able to go one step further and just build the object literal directly. This of course is observable (Object.prototype setters would not be invoked), but would result in ideal code for most cases. You'd only be able to do this for cases like modules, though.
"use strict";
// Hoist `bar`
function bar() { return 1; }
var Foo = {
// `namespace Foo {`
bar: bar,
// `}`
// `enum Foo {`
// `One,`
One: 0,
0: "One",
// `Two,`
Two: 1,
1: "Two",
// `Three,`
Three: 2,
2: "Three",
// `}`
}
Do want to note that while this can technically be larger for a few enums, it'll only be that for a few, and it'll almost certainly be a wash after compression as well.
📃 Motivating Example
This reduces namespace and enum code gen overhead significantly and also make it much easier to optimize for both engines (on startup) and optimizer tools.
Currently, Terser has two issues around TypeScript enum generation, and this would serve to fix both.
The first issue linked, https://github.com/terser/terser/issues/1064, features this code:
enum FooEnum {
ONE,
TWO,
THREE
}
console.log(FooEnum.ONE, FooEnum[0])
The current emit when targeting modules is this:
var FooEnum;
(function (FooEnum) {
FooEnum[FooEnum["ONE"] = 0] = "ONE";
FooEnum[FooEnum["TWO"] = 1] = "TWO";
FooEnum[FooEnum["THREE"] = 2] = "THREE";
})(FooEnum || (FooEnum = {}));
console.log(FooEnum.ONE, FooEnum[0]);
Terser, with --module --mangle --compress passes=2, minifies it to this (whitespace added for clarity):
var E;
!function(E){
E[E.ONE=0]="ONE",
E[E.TWO=1]="TWO",
E[E.THREE=2]="THREE"
}(E||(E={})),
console.log(E.ONE,E[0]);
My proposed emit would be this:
var FooEnum = {};
FooEnum.ONE = 0;
FooEnum[0] = "ONE";
FooEnum.TWO = 1;
FooEnum[1] = "TWO";
FooEnum.THREE = 2;
FooEnum[2] = "THREE";
console.log(FooEnum.ONE, FooEnum[0])
Terser with the same settings compresses it to just this:
var o=0,l="ONE";console.log(o,l);
Adding one more pass (--compress passes=3) allows it to complete the enum inlining:
console.log(0,"ONE");
Worth noting that Terser by default only performs one pass. This should probably be called out in whatever blog post for visibility.
💻 Use Cases
The current approach just quite frankly is extremely difficult to optimize for. Not only is it bloated, but it's also difficult to detect for minification purposes - the motivating example elaborates on this further.
It'd also boost startup speed, even absent the minifier optimizations, since it's only setting properties, not also going through the ceremony of IIFEs plus Enum || (Enum = {}).
Also, consider this code:
export enum Foo {
One,
Two,
Three,
}
The code it generates is this:
export var Foo;
(function (Foo) {
Foo[Foo["One"] = 0] = "One";
Foo[Foo["Two"] = 1] = "Two";
Foo[Foo["Three"] = 2] = "Three";
})(Foo || (Foo = {}));
Terser, with --module --mangle --compress passes=2, minifies it to this (whitespace added for clarity):
export var Foo;
!function(o){
o[o.One=0]="One",
o[o.Two=1]="Two",
o[o.Three=2]="Three"
}(Foo||(Foo={}));
My proposed emit would be this:
// Proposed
export var Foo = {};
Foo.One = 0;
Foo[0] = "One";
Foo.Two = 1;
Foo[1] = "Two";
Foo.Three = 2;
Foo[2] = "Three";
// Ideal
export var Foo = {
One: 0,
0: "One",
Two: 1,
1: "Two",
Three: 2,
2: "Three",
};
Once https://github.com/terser/terser/issues/1389 gets resolved (it's about the export specifically), Terser should minify the proposed one to this:
export var Foo={One:0,0:"One",Two:1,1:"Two",Three:2,2:"Three"};
The "ideal" code would just minify to this without the complicated multi-pass processing.
With esbuild --minify, it's not as sophisticated, but benefits are still apparent. Here are the current and new emits side-by-side with it, to show the difference:
// Current
export var Foo;(function(e){e[e.One=0]="One",e[e.Two=1]="Two",e[e.Three=2]="Three"})(Foo||(Foo={}));
// Proposed
var e={};e.One=0,e[0]="One",e.Two=1,e[1]="Two",e.Three=2,e[2]="Three";export{e as Foo};
// Ideal
export var Foo={One:0,0:"One",Two:1,1:"Two",Three:2,2:"Three"};
Guida per i contributori
Apri la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Direzione di ricerca
Non vengono indicati file del repository, test o punti di ingresso del compilatore. Inizia individuando l’implementazione dell’emissione di enum e namespace e i relativi test esistenti, quindi confronta l’output generato per gli esempi nell’issue; il lavoro è completato quando l’output proposto con un overhead inferiore conserva il comportamento osservabile dichiarato e supera i test di emit pertinenti.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- javascript, typescript
- Ambito
- compilers, performance
- Tipo di issue
- Funzionalità
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Stato di attività
- Ferma
- Chiarezza
- Abbastanza chiara
- Idoneità per principianti
- 28/100