question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Proposal: Conditional Compilation

See original GitHub issue

Proposal: Conditional Compilation

Problem Statement

At design time, developers often find that they need to deal with certain scenarios to make their code ubiquitous and runs in every environment and under every runtime condition. At build time however, they want to emit code that is more suited for the runtime environment that they are targetting by not emitting code that is relevant to that environment.

This is directly related to #449 but it also covers some other issues in a similar problem space.

Similar Functionality

There are several other examples of apporaches to solving this problem:

Considerations

Most of the solutions above use “magic” language features that significantly affect the AST of the code. One of the benefits of the has.js approach is that the code is transparent for runtime feature detection and build time optimisation. For example, the following would be how design time would work:

has.add('host-node', (typeof process == "object" && process.versions && process.versions.node && process.versions.v8));

if (has('host-node')) {
    /* do something node */
}
else {
    /* do something non-node */
}

If you then wanted to do a build that targeted NodeJS, then you would simply assert to the build tool (staticHasFlags) that instead of detecting that feature at runtime, host-node was in fact true. The build tool would then realise that the else branch was unreachable and remove that branch from the built code.

Because the solution sits entirely within the language syntax without any sort of “magical” directives or syntax, it does not take a lot of knowledge for a developer to leverage it.

Also by doing this, you do not have to do heavy changes to the AST as part of the complication process and it should be easy to identify branches that are “dead” and can be dropped out of the emit.

Of course this approach doesn’t specifically address conditionality of other language features, like the ability to conditionally load modules or conditional classes, though there are other features being introduced in TypeScript (e.g. local types #3266) which when coupled with this would address conditionality of other language features.

Proposed Changes

In order to support conditional compile time emitting, there needs to be a language mechanic to identify blocks of code that should be emitted under certain conditions and a mechanism for determining if they are to be emitted. There also needs to be a mechanism to determine these conditions at compile time.

Defining a Conditional Identifier at Design Time

It is proposed that a new keyword is introduced to allow the introduction of a different class of identifier that is neither a variable or a constant. Introduction of a TypeScript only keyword should not be taken lightly and it is proposed that either condition or has is used to express these identifiers. When expressed at design time, the identifier will be given a value which can be evaluated at runtime, with block scope. This then can be substituted though a compile time with another value.

Of the two keywords, this proposal suggests that has is more functional in meaning, but might be less desirable because of potential for existing code breakage, but examples utlise the has keyword.

For example, in TypeScript the following would be a way of declaring a condition:

has hostNode: boolean = Boolean(typeof process == "object" && process.versions && process.versions.node && process.versions.v8);

if (hostNode) {
    console.log('You are running under node.');
}
else {
    console.log('You are not running under node.');
}

This would then emit, assuming there is no compile time substitutional value available (and targeting ES6) as:

const hostNode = Boolean(typeof process == "object" && process.versions && process.versions.node && process.versions.v8);
if (hostNode) {
    console.log('You are running under node.');
}
else {
    console.log('You are not running under node.');
}

Defining the value of a Conditional Identifier at Compile Time

In order to provide the compile time values, an augmentation of the tsconfig.json is proposed. A new attribute will be proposed that will be named in line with the keyword of either conditionValues or hasValues. Different tsconfig.json can be used for the different builds desired. Not considered in this proposal is consideration of how these values might be passed to tsc directly.

Here is an example of tsconfig.json:

{
    "version": "1.6.0",
    "compilerOptions": {
        "target": "es5",
        "module": "umd",
        "declaration": false,
        "noImplicitAny": true,
        "removeComments": true,
        "noLib": false,
        "sourceMap": true,
        "outDir": "./"
    },
    "hasValues": {
        "hostNode": true
    }
}

Compiled Code

So given the tsconfig.json above and the following TypeScript:

has hostNode: boolean = Boolean(typeof process == "object" && process.versions && process.versions.node && process.versions.v8);

if (hostNode) {
    console.log('You are running under node.');
}
else {
    console.log('You are not running under node.');
}

You would expect the following to be emitted:

console.log('You are running under node.');

As the compiler would replace the symbol of hostNode with the value provided in tsconfig.json and then substitute that value in the AST. It would then realise that the one of the branches was unreachable at compile time and then collapse the AST branch and only emit the reachable code.

Issue Analytics

  • State:open
  • Created 8 years ago
  • Reactions:329
  • Comments:71 (17 by maintainers)

github_iconTop GitHub Comments

35reactions
SomaticITcommented, Mar 18, 2016

Hi everyone,

Conditional compilation is a must-have feature in Typescript. The idea of both runtime et precompilation time constants is also a very good idea.

But I think we should use a C#/C++ syntax but adapted to JavaScript :

Sample source

#define HOST_NODE (typeof process == "object" && process.versions && process.versions.node && process.versions.v8))
#define COMPILE_OPTIONS ["some", "default", "options"]

#if HOST_NODE
console.log("I'm running in Node.JS");
#else
console.log("I'm running in browser");
#endif

#if COMPILE_OPTIONS.indexOf("default") !== -1
console.log("Default option is configured");
#endif

Edit: Remove equals signs to keep C-style syntax as suggested by @stephanedr .

Runtime compilation

This would then emit, assuming there is no compile time substitutional value available (and targeting ES6) as:

const __tsc__HOST_NODE = (typeof process == "object" && process.versions && process.versions.node && process.versions.v8));
const __tsc__COMPILE_OPTIONS = ["some", "default", "options"];

if (__tsc__HOST_NODE) {
    console.log("I'm running in Node.JS");
} else {
    console.log("I'm running in browser");
}

if (__tsc__COMPILE_OPTIONS.indexOf("default") !== -1) {
    console.log("Default option is configured");
}

tsconfig.json configuration

Assuming you define some constants in your tsconfig.json :

{
    "defines": {
        "HOST_NODE": false,
        "COMPILE_OPTIONS": ["some", "other", "options"]
    }
}

This would then emit as :

console.log("I'm running in browser");

CLI configuration

Or if you define contants in an other way by using CLI :

$ tsc --define=HOST_NODE:true --define=COMPILE_OPTIONS:["some", "default", "options"]

This would then emit as :

console.log("I'm running in NodeJS");
console.log("Default option is configured");

Typings emitting and interpretation

Assuming you have a module designed like this :

#define HOST_NODE (typeof process == "object" && process.versions && process.versions.node && process.versions.v8))
#define COMPILE_OPTIONS ["some", "default", "options"]

export function commonFunction() { }

#if HOST_NODE
export function nodeSpecificFunction() { }
#endif

#if COMPILE_OPTIONS.indexOf("default") !== -1
export function dynamicOptionFunction() { }
#endif

export class MyClass {
    common() { }
#if HOST_NODE
    nodeSpecific() { }
#endif
}

Edit: Remove equals signs to keep C-style syntax as suggested by @stephanedr . Edit: Added Class case as suggested by @stephanedr .

If no definitions are configured, it should be interpreted like this :

export function commonFunction(): void;
export function nodeSpecificFunction?(): void;
export function dynamicOptionFunction?(): void;
export class MyClass {
    common(): void;
    nodeSpecific?(): void;
}

Edit: Added Class case as suggested by @stephanedr .

If you define some constants in your tsconfig.json :

{
    "defines": {
        "HOST_NODE": false,
        "COMPILE_OPTIONS": ["some", "default", "options"]
    }
}

Then, it should be interpreted like this :

export function commonFunction(): void;
export function dynamicOptionFunction(): void;
export class MyClass {
    common(): void;
}

Edit: Added Class case as suggested by @stephanedr .

It allows compiler and EDIs to ignore some parts of the code based on compiler configuration.

Function-like syntax

Based on @stephanedr comments.

Assuming following sample source

#define DEBUG !!process.env.DEBUG
#if DEBUG
function _assert(cond: boolean): void {
    if (!cond)
        throw new AssertionError();
}
#define assert(cond: boolean): void _assert(cond)
#endif

type BasicConstructor = { new (...args: Object[]) => T };
#if DEBUG
function _cast<T>(type: BasicConstructor, object: Object): T {
    #assert(object instanceof type);
    return <T>object;
}
#define cast<T>(type: BasicConstructor, object: T) _cast(type, object)
#else
#define cast<T>(type: BasicConstructor, object: T) <T>object
#endif

class C {
    f(a: number) {
        #assert(a >= 0 && a <= 10);
        let div = #cast(HTMLDivElement, document.getElementById(...));
    }
}

This would then emit, assuming there is no compile time substitutional value available (and targeting ES6) as:

const __tsc__EMPTY = function () { return; };
const __tsc__DEBUG= !!process.env.DEBUG;

let __tsc__assert = __tsc__EMPTY;
if (__tsc__DEBUG) {
    function _assert(cond) {
         if (!cond)
             throw new AssertionError();
    }
    __tsc__assert = function (cond) { return _assert(cond); };
}

let __tsc__cast = __tsc__EMPTY;
if (__tsc__DEBUG) {
    function _cast(type, object) {
        __tsc__assert(object instanceof type);
        return object;
    }
    __tsc__cast = function (type, object) { return _cast(type, object); };
}
else {
    __tsc__cast = function (type, object) { return object; };
}

class C {
    f(a) {
        __tsc__assert(a >= 0 && a <= 10);
        let div = __tsc__cast(HTMLDivElement, document.getElementById(...));
    }
}

Assuming you define DEBUG constant with value true using CLI or typings.json , this would then emit as :

function _assert(cond) {
    if (!cond)
       throw new AssertionError();
}

function _cast(type, object) {
    _assert(object instanceof type);
    return object;
}

class C {
    f(a) {
        _assert(a >= 0 && a <= 10);
        let div = _cast(HTMLDivElement, document.getElementById(...));
    }
}

Now, assuming you define DEBUG constant with value false using CLI or typings.json , this would then emit as :

class C {
    f(a) {
        let div = document.getElementById(...);
    }
}

Conclusion

I think it allows a more granular conditional compilation by using the power of JavaScript. Compiler can evaluate expressions passed by compiler directives #if #elseif

Moreover it clearly separates (in both code-style and evaluation) the compiler directives from your code, like it used to be on C-style compiled languages.

What do you think ? Should I start a new issue to avoid confusion ?

25reactions
stephanedrcommented, Jun 18, 2015

I would also like that TS supports conditional compilation, but more in the C++/C# way too. My main use is for assertions. With your proposal @kitsonk, I would be able to empty a function, but not remove its call, e.g.:

has debug = true;
var assert = debugMode ? function (cond: boolean) { if (cond) throw new AssertionError(); }
                       : function (cond: boolean) { };

With a C++/C# implementation:

#if DEBUG
function _assert(cond: boolean) { if (cond) throw new AssertionError(); }
#define assert(cond) _assert(cond)
#else
#define assert(cond)
#endif

Additionally, if TS would provide “macros” for the current filename and line number, I would be able to retrieve them relative to the TS source code, whereas today a stack trace reports line numbers relative to the generated JS file…

Some points from your proposal:

  • You seem to consider such a flag as constant, so what is the interest in keeping the 2 alternatives (if/else) when no substitution is given (its value cannot be changed at runtime)? For me the dead code elimination should apply as well.
  • Instead of introducing a new keyword has, I think we could stay with const and allow all constants to be overridable at compile time (and apply dead code elimination with any constant expressions).
Read more comments on GitHub >

github_iconTop Results From Across the Web

Proposal: Lightweight conditional compilation extension
Proposal : Lightweight conditional compilation extension ... Hi all,. Recently I did some work to make rescript compiler not depending on a patched ......
Read more >
SE-0367: Conditional compilation for attributes - Swift Forums
Hi everyone. The review of SE-0367, Conditional compilation for attributes, begins now and runs through August 15st, 2022.
Read more >
TypeScript - Visual Studio / MSBuild conditional compilation
Conditional Compilation is not yet available in TypeScript, some requests and proposals have been made: Support conditional compilation ...
Read more >
Proposal: Add CONTRACTS_FULL to regular conditional ...
... in the project properties, the symbol CONTRACTS_FULL should also be added to/ deleted from the project (XML file) as a conditional compilation...
Read more >
Conditional compilation exclusive or - Rust Internals
I propose a new term, "one" which requires exactly one of the list of attributes, e.g. #[cfg(not(one(color_red, color_green, color_blue)))] ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found