TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Feature Request: Macros

Open Gaelan opened this issue 9 years ago • 88 comments

This one might be waaaaaay out of scope, but I think it is worth proposing. The idea here is to add support for macros, functions that run at compile time, taking one or more AST nodes and returning an AST node or array of AST nodes. Examples/use cases: (Syntax off the top of my head, open to better ideas)

Interface-Based Validation

// validation.macros.ts
function interfaceToValidatorCreator(interface: ts.InterfaceDeclaration): ts.Statement {
    // Would return something like "const [interface.name]Validator = new Validator({...});",
    // using types from the interface
}
macro CreateValidator(interface: ts.InterfaceDeclaration) {
    return [interface, interfaceToValidator(interface)];
}
// mainFile.ts
/// <macros path='./valididation.macros.ts' />
@#CreateValidator // Syntax 1: Decorator-Style
interface Person {
    name: string;
    age: number;
}

PersonValidator.validate(foo)

Type Providers

// swagger.macros.ts
macro GetSwaggerClient(url: ts.StringLiteral): AssertionExpression {
    // return something like "new SwaggerClient([url]) as SwaggerClientBase & {...}" where
    // ... is an object creating the methods generated from the URL.
}
// mainFile.ts
/// <macros path='./swagger.macros.ts' />
var fooClient = #GetSwaggerClient("http://foo.com/swagger.json"); // Syntax 2: Function call syntax
fooClient.getPeople((people) => {
    people.every((person) => console.log(person.firstName + "," + person.lastName);
});

Conditional Compilation

// conditional-compilation.macros.ts
macro IfFlagSet(flagName: ts.StringLiteral, code: ts.Statement[]): ts.Statement[] {
    return process.env[flagName.string] ? code : []
}
// mainFile.ts
/// <macros path='./compilation.macros.ts' />
#IfFlagSet("DEVELOPMENT") { // Syntax 3: Language Construct-Like (multiple arguments can be passed in parentheses)
    expensiveUnnecessarySanityCheck()
}

Notes

  • Macros would run right after parsing. Not sure how we would deal with macros that need type information.
  • This would make running tsc on unknown code as dangerous as running unknown code. It might be good to require a --unsafeAllowMacros argument, not settable from a tsconfig.json.
  • It might be worth nothing in the docs that the AST format may change at any time, or something along those likes
  • The macro keyword would probably compile to a function, followed by a ts.registerMacro(function, argumentTypes, returnType call.
  • Macros must be typed as returning a AST interface. This means that functions creating ASTs will probably need to have an explicit return type (or a calling function could have an explicit return type.
    • Alternatively, we could consider giving the kind property special treatment in macros.ts files.
  • Just because a proposed syntax looks like a normal typescript construct doesn't mean it behaves like one. #Foo(interface Bar{}) is valid syntax, as long as there is a macro named Foo that takes an interface.
    • Exception: The Decorator syntax might need to be a bit more choosy (no decorating 1 + 1, but decorating Interfaces, interface items, functions, etc. should be fine.
  • This issue is likely to be updated quite a bit. For a log of changes, see the gist

Gaelan avatar Sep 20 '15 22:09 Gaelan

duplicate of #3136?

mhegazy avatar Sep 21 '15 08:09 mhegazy

@mhegazy I don't think so. Support for macros as AST->AST functions would let us do anything you could do with a type provider (just return an AssertionExpression, see the second example in the issue text), but also conditional compilation (return the passed code if the condition is true, otherwise do nothing), as well as general boilerplate reduction.

Gaelan avatar Sep 21 '15 14:09 Gaelan

+1

arkdemon avatar Nov 28 '15 14:11 arkdemon

+1

fgalassi avatar Dec 14 '15 21:12 fgalassi

+1 (I almost expected it to work with sweet.js.)

elbarzil avatar Dec 23 '15 01:12 elbarzil

+1

ulfryk avatar Dec 29 '15 11:12 ulfryk

+1

nisimjoseph avatar Jan 09 '16 18:01 nisimjoseph

You can also look at the haxe macro system how they implemented it. http://haxe.org/manual/macro.html

joergwasmeier avatar Jan 10 '16 09:01 joergwasmeier

+1

soycabanillas avatar Jan 14 '16 19:01 soycabanillas

+1 :+1:

Zorgatone avatar Jan 22 '16 11:01 Zorgatone

Question Could this be provided by a pre-compile hook; between slurping the .ts file and depositing the .js file?

Possible Benefits

  • Allows for competing macro processors, eventually yielding a best-in-class ? or 500 of them...
  • Natural requirement is production of valid TypeScript; but that's now on the user
  • Allowed to evolve separately and distinctly along a rigorous path for macro definitions and implementations (ala regular expressions)

Of course, I could be missing some fundamental concept of macros and compilers, rather than just attempting to break up the traditional view.

carywreams avatar Feb 10 '16 14:02 carywreams

+1

lazdmx avatar Feb 25 '16 08:02 lazdmx

+1

gvkhna avatar Apr 12 '16 23:04 gvkhna

I need this. My needed use-case right now would be to make sort-of an "inline-function", or similar to a C-preprocessor-like parametric macro. That code would be inlined to avoid the function call on the JavaScript output.

C Macro Example:

#define ADD(x, y) ((x) + (y))

C inline Example:

inline int ADD(int x, int y) {
    return x + y;
}

I'd write something similar in TypeScript (let's assume the keyword inline will work only with functions that can be inlined).

In TypeScript the inline approach would look like this:

inline function ADD(x: number, y: number) {
    return x + y;
}

UPDATE: Looks like a similar issue was already here #661

Zorgatone avatar Apr 21 '16 13:04 Zorgatone

I would enjoy some form of macros for sure. As great as typescript is, it's still crappy old JS underneath. Something I would want to macro in would be proper if/else expressions.

AlexGalays avatar Aug 04 '16 15:08 AlexGalays

👍

crazyquark avatar Sep 14 '16 07:09 crazyquark

migrated from #11536 (which was closed in favor of this one)

situation:

  • sometimes i wish i could generate a piece of code based on some existing one, for example a construction function for an given interface

    interface MyConfig {
       name: string;
       values: number[];
    }
    function myConfigFrom(name: string, values: number[]) : MyConfig {
       return { name, values };
    }
    

problem: currently my options are

  • either write it by hands (tedious monotonous work)
  • put together a homemade code generator and run it as a pre-build step (lot of maintenance, non-standard)

solution:

  • allow AST rewrites via decorators

    @rewrite(addContructorFunction)
    interface MyConfig {
       name: string;
       values: number[];
    }
    function addContructorFunction(node: ts.Node): ts.Node[] {
       return [node, toConstructorFunction(node as ts.InterfaceDeclaration)];
    }
    function toConstructorFunction(node: ts.IntefaceDeclaration): ts.FunctionDeclaration {
       // fun stuff goes here
    }
    

zpdDG4gta8XKpMCd avatar Oct 11 '16 16:10 zpdDG4gta8XKpMCd

This would be huge. In something like Scala, macros are a way for the community to implement and test out new language features that are not yet (or will never be) supported by the core language.

After adding macro support, TS would have a large laboratory of potential features to draw on when implementing new ones, and could gauge support and feasibility of a feature before implementing it.

Features like pattern matching could first be implemented as macros, and then either moved into a standard macro lib, or into TS core if they are broadly useful and popular. This takes a burden off TS maintainers and authors, and gives the community freedom to experiment without forking the TS compiler.

bcherny avatar Oct 13 '16 20:10 bcherny

FWIW, I think that a more promising direction is for a macro facility to accommodate TS. The obvious example would be to extend sweet.js so it accepts the TS syntax, and expands into TS code. This way, TS doesn't need to know about macros at all.

This leads to something very similar to Typed Racket (for anyone who knows that), including the minor disadvantage of not being able to write macros that depend on types.

elibarzilay avatar Oct 14 '16 14:10 elibarzilay

@elibarzilay With that approach, would macros be typesafe? If the whole point of TS is to be a typesafe layer on top of JS, macros should ideally also be typesafe.

Again comparing to Scala macros, their safety is a huge win. Otherwise you end up shooting in the dark without IDE/compiler support until you get something that compiles.

bcherny avatar Oct 14 '16 16:10 bcherny

@bcherny: The macro code itself wouldn't be typed. But that's minor IMO (since at that level it's all ASTs in and out). (Compared to random scala macros that I've seen after a few seconds of grepping the web, you get only Expr with no type qualification.)

The code that macros produce might not be well typed, but it still goes through the type checker which does verify that the result is safe.

elibarzilay avatar Oct 14 '16 19:10 elibarzilay

I think this is something similar to c/c++ perprocessor maybe, with type check? But i really want to write something like that:

#IfFlagSet("DEVELOPMENT") {
    macro assert(cond: any, message?: string) {
          if (!cond) { throw new Error("...") }
    }
} else {
   macro assert(...x: any[])  // or something similar, and in this case dont emit code for this macro call
}

zozzz avatar Oct 27 '16 20:10 zozzz

(Similar, but a proper macro system compared to CPP is like comparing JS to machine code...)

elibarzilay avatar Oct 27 '16 21:10 elibarzilay

+1

TobiasHeidingsfeld avatar Dec 08 '16 12:12 TobiasHeidingsfeld

Hygienic macro. https://en.wikipedia.org/wiki/Hygienic_macro

wiltonlazary avatar Dec 22 '16 09:12 wiltonlazary

JS is a mixed bag. It has some nice features, and some really awful ones. Also, new features take a very long time to get voted, approved by TC39 then implemented by the browsers. Political agendas may sometimes block some great features.

Macros could help us implement some very useful things in user land. I would love to use this right now: https://github.com/mindeavor/es-pipeline-operator

AlexGalays avatar Jan 19 '17 09:01 AlexGalays

+1

My example is one of having interfaces (that also use extends) that describe websocket network messages with binary specific types (type int8 = number), and wanting to generate code for something like https://github.com/phretaddin/schemapack

Interestingly I've done exactly this without macros and it was a pain in the rear both for me and future developers to the project. But it also saved an incredible amount of time considering there were over 100 different network message interfaces.

Coderah avatar Mar 02 '17 22:03 Coderah

Another use case: Given a simple or discriminated union, generate the list of all the possible values. This can help with mistakes where things are modified in one place but not the other.

AlexGalays avatar Mar 10 '17 11:03 AlexGalays

+1

fis-cz avatar Apr 24 '17 18:04 fis-cz

+1

winksaville avatar Apr 29 '17 14:04 winksaville