Skip to content

Add --inlineConsts flag #6805

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
RyanCavanaugh opened this issue Feb 1, 2016 · 21 comments
Closed

Add --inlineConsts flag #6805

RyanCavanaugh opened this issue Feb 1, 2016 · 21 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@RyanCavanaugh
Copy link
Member

New flag --inlineConsts

Input:

const x = 'hello';

console.log(x);

Output:

console.log('hello');

See also #6678 + #6804

TBD: It's not clear you would want longer strings to inline into their use sites. Discuss.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Help Wanted You can do this Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". labels Feb 1, 2016
@RyanCavanaugh RyanCavanaugh added this to the Community milestone Feb 1, 2016
@weswigham
Copy link
Member

Is this only expected to inline strings/numbers? Regex literals? Template strings?

@RyanCavanaugh
Copy link
Member Author

Only "compile-time computable constants", i.e. all the things that are valid const enum initializers, concatenations of string literals, and true / false.

@Andael
Copy link

Andael commented Mar 4, 2016

In the above example, the declaration of the const disappears. That could cause problems in a namespace.

Given a namespace:

// vec3.ts

namespace vec3 {
    export const SIZE = 3
    export function create(x, y, z) {...}
    }

And code that uses it, expecting the constant to be there:

// buffer-helpers.ts

interface VectorType {
    SIZE: number
    create: Function
    }

function read(buffer: Float32Array, type: VectorType) {
    // do something that uses type.SIZE
    }

read(someBuffer, vec3)

Output code, following the suggested rules:

// app.js

(function(vec3) {
    vec3.create = function(x, y, z) {...}
    })(vec3)

function read(buffer, type) {
    // do something that uses type.SIZE
    }

read(someBuffer, vec3)    // will fail in some unexpected manner due to missing vec3.SIZE

@RyanCavanaugh
Copy link
Member Author

Hrm, good point. Maybe it's only safe to remove consts that are defined in the global scope?

@Andael
Copy link

Andael commented Mar 5, 2016

TBD: It's not clear you would want longer strings to inline into their use sites. Discuss.

Is the goal to optimize by file size, load/parse speed, or execution speed?

@mhegazy
Copy link
Contributor

mhegazy commented Mar 5, 2016

constant folding usually targets execution speed. the file size will probably increase if you have that constant used in a few places.

@Andael
Copy link

Andael commented Mar 6, 2016

Hrm, good point. Maybe it's only safe to remove consts that are defined in the global scope?

Ideally, it could still inline uses of a module-scope constant, even if it keeps the declaration there. Would that be sensible?

Only "compile-time computable constants"

What would be a suitable way to determine which functions are compile-time computable? Would it be enough to say any method on String/Array/Number/Boolean that doesn't have "locale" in the name?

@Naharie
Copy link

Naharie commented Aug 8, 2016

@RyanCavanaugh
@ander-nz

On #6805 (comment), and #6805 (comment) maybe we should add some new comment form like: /* (const) {ConstantName} ({ConstantValue}) */.

So:

// vec3.ts

namespace vec3 {
    export const SIZE = 3
    export function create(x, y, z) {...}
    }

Would be adjusted to be:

// vec3.ts

/* (const) vec3.SIZE (3) */

namespace vec3 {
    export const SIZE = 3
    export function create(x, y, z) {...}
    }

For completeness I included the sample client code

// buffer-helpers.ts

interface VectorType {
    SIZE: number
    create: Function
    }

function read(buffer: Float32Array, type: VectorType) {
    // do something that uses type.SIZE
    }

read(someBuffer, vec3)

The compiler would see the comment and emit some helper like this:

var __With = function(Target, Name, Value)
{
        Target[Name] = Value;
        return (Target);
};

Which would be used like:

(function(vec3) {
    vec3.create = function(x, y, z) {...}
    })(vec3)

function read(buffer, type) {
    // Other code
   while (SomeVar < type.SIZE)
   {
         /* Code */
    }
    // Other code
 }

read(someBuffer, __With (vec3, "SIZE", 3)) 

Could that work or am I just missing all the corner cases?

@dead-claudia
Copy link

@KingDavid12 Your generated code is semantically equivalent to the below:

vec3.create = function(x, y, z) {...}

function read(buffer, type) {
    // Other code
   while (SomeVar < type.SIZE)
   {
         /* Code */
    }
    // Other code
 }

read(someBuffer, (vec3.SIZE = 3, vec3))

There's 0 constant folding going on here, so you don't have the runtime benefit of minimal computation.

@dead-claudia
Copy link

dead-claudia commented Oct 29, 2016

@RyanCavanaugh @mhegazy

Should strings be excluded from the constant propagation after a certain limit? There's usually minimal perf difference in practice, and larger strings sometimes are faster when stored in a variable on the heap.

Not arguing numbers, etc., though. Those can still be folded as normally.

@alitaheri
Copy link

This feature can actually decrease file size sometimes when combined with tools like uglify. Usually with conditionals:

const myConst = "blah";

// ...

// this code can be safely removed after myConst gets replaced with "blah"
if (myConst !== "blah") {
 // ...
}

@Naharie
Copy link

Naharie commented Oct 29, 2016

@isiahmeadows Good point. Maybe what can be done is to inline constants internally in the namespace but leave the definition. That would allow later code using the library to see the constant definition and inline it themselves.

@dead-claudia
Copy link

@KingDavid12

On Sat, Oct 29, 2016, 11:02 KingDavid12 notifications@github.com wrote:

@isiahmeadows https://github.com/isiahmeadows Good point. Maybe what can
be done is to inline constants internally in the namespace but leave the
definition. That would allow later code using the library to see the
constant definition and inline it themselves.

That would have to be done to some extent regardless, in order to properly
inline everything. ;-)


You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub
#6805 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AERrBDkIxxiK_VfMrSh5WxdPNfQFjkIPks5q41_0gaJpZM4HRCgy
.

@ghost
Copy link

ghost commented Sep 29, 2017

A const string enum would seem to do this pretty well?

const enum X { x = "hello" }

console.log(X.x);

becomes:

"use strict";
console.log("hello" /* x */);

@Kingwl
Copy link
Contributor

Kingwl commented Jan 26, 2018

i 'd like to work on this
but I may only make a minimal implement

i'm going to add a inlineConstantTransformer or add transform on tsTransformer

resolve and mark identifier if it can be inline

update identifier node to resolved node

Is there any problem with this? need help plz 🙏

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus and removed Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Help Wanted You can do this labels Sep 5, 2018
@RyanCavanaugh
Copy link
Member Author

I think we want to re-evaluate the necessity of this

@RyanCavanaugh RyanCavanaugh added the Help Wanted You can do this label Mar 7, 2019
@RyanCavanaugh RyanCavanaugh modified the milestones: Community, Backlog Mar 7, 2019
@Raiondesu
Copy link

Raiondesu commented Jul 8, 2019

What about not solving this with a separate flag?
It's obvious that in most cases it's desired to have only some of const definitions removed at compile-time.

I'd suggest putting the declare keyword to a good use here. Now, the declare keyword is used to, basically, tell the compiler that there is a variable lying around in a global scope. Therefore, the declare keyword doesn't allow any implementation.
However, we could use this limitation of declare to expand its functionality: if an implementation is followed for declare statements - this implementation is then inlined at compile-time.
Like this:

// this variable exists somewhere, isn't inlined
declare const hello;

// we want to inline this function's implementation
declare const isHello = (v): v is 'hello' => v === 'hello';
// this is inlined later
declare const world = 'world';

if (isHello(hello)) {
  console.assert(hello[0] === 'h')
  console.log(hello, world)
}
if (hello === 'hello') {
  console.assert(hello[0] === 'h')
  console.log(hello, 'world')
}

There's clearly a use-case for this. For example, I write many utility functions just for type-safety, most of which would evaluate to typeof x === something && x === someOtherThing or Array.isArray(x) && typeof x[0] === something or other very simple and atomic operations. These utility functions can greatly increase final build size, and I'd be happy if I could inline them.

I would not want to inline ALL consts, however. And one of the reasons for this is reference-safety.
Consider the following:

// someObject.ts
const someObject = {
  type: 'object'
};

export default someObject;
// index.ts
import someObject from 'someObject';

let obj = someObject; // default value for `obj`

if (/* some condition */) {
  // assign something to `obj`
}

// if `obj` is still its default value
if (obj === someObject) {
  obj = something;
}
// etc.

After inlining consts, using proposed --inlineConsts flag, this code would work differently than vanilla JS,
because JS will create two different objects with value of { type: 'object' }.
Therefore, the condition if (obj === someObject) will always fail, if consts are inlined, because { type: 'object' } === { type: 'object' } will always be false.

EDIT: here's a playground with a small executable demo of what I mean.

Of course, it's possible to maybe not inline consts that are used like this... but won't it be too much cognitive load to always remember which of your consts are inlined and which are not?

The alternative use of declare would make this so much simpler, I think.

This would actually behave a lot like C++ #define statements, which are also very familiar to many people.

@RyanCavanaugh RyanCavanaugh removed the Help Wanted You can do this label Jul 8, 2019
@dead-claudia
Copy link

@Raiondesu declare in TS is about declaring bindings, specifically bindings with no immediate value, not changing their instantiation.

In my experience, I've never once stumbled across a use case where a primitive constant would've been better not inlined, even in perf-critical code. If you're worried about duplication, gzip can make quick work of that. About the only two places I could imagine it being useful to not inline a constant is in embedded applications and with very large strings used in multiple unrelated places. In the first, you need enough control that you'd do better (ab)using const enums for constants than using const + --inlineConsts. In the second, you can work around it with wrapping it in an object, but it doesn't appear large strings are guaranteed to make it, either.

@Raiondesu
Copy link

Raiondesu commented Jul 9, 2019

@isiahmeadows, I understand your point. It's fair. But...

  1. Personal experience is just that. Personal. If you never needed something - doesn't mean nobody needs it.
  2. All of your solutions are just workarounds, which do not cover the problem to full extent and could lead to poor code quality in the long run.
  3. The semantics of declare are only present in the "type-realm" of TS, so to speak. If it were given semantics in the "value-realm" - these semantics would not overlap with the "type-realm" ones. The most obvious example of a similar keyword is typeof which means one thing for values (getting their string typename) and different for types (getting the original type of a value/instance). The same logic can be applied to declare without hurting anybody (including backwards compatibility):
  • declare in "type-realm" would still be about declaring bindings.
  • declare in "value-realm" would be about declaring an alias for a piece of code.

In my comment I never stated that I'm concerned about code duplication or performance (however your case of randomly placed long strings is actually pretty common and would benefit from a keyword solution).

My case was that it would be useful, if consts could inline not only simple primitives, but also complex expressions (like functions or objects). And that it also could be useful if you could atomically control which consts need to be inlined. If in your practice it's actually useful to inline all of them - I don't see any problem with declare either - it could quickly become a second nature for you to just write declare const everywhere. And this would bring clarity too, because there's now a clear distinction between real (const) and inlined (declare const) values.

In the case of a compilation flag, however, it would not only break the vanilla js behavior (I mentioned it in the previous comment), but also make it a complete disaster for readability, since you'd never know by heart (intuitively) which consts in your code are inlined and which aren't. One of TS goals is to not break the semantics of vanilla js. And introducing the inlineConsts flag certainly would disobey this goal. Adding new semantics to a TS-only keyword (declare), however, wouldn't.

If adding some semantics to declare is too mind-blowing (typeof aside) - ok, we could just add another keyword wastefully (as if there weren't too many of them already).

@chengB12
Copy link

I have another use case for this suggestion, when browser-facing code need reference constants include const list and object which inside module that not designed for browser.

// index.js of module foo which is huge
export const bar = [1,2,3] as const
// module that intended to run in browser
import { bar } from 'foo'
if (bar.include(x)) ...

which on compile with --inlineConsts would emit code

if ([1,2,3].include(x)) ...

this can avoid import huge module to browser, while no need to make hard copy of bar to code

@RyanCavanaugh
Copy link
Member Author

This is clearly out of scope. Bad idea, Ryan.

@RyanCavanaugh RyanCavanaugh closed this as not planned Won't fix, can't repro, duplicate, stale Mar 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

10 participants