itsdouges/typescript-transformer-handbook

Usage question: Changing type checking of operators

cshaa opened this issue · 4 comments

cshaa commented

I'm frequently working with symbolic mathematical expressions. Sadly, JavaScript doesn't support operator overloading, so I have to write a lot of ugly code like add(u, mul(2, v)). I would love to make a transformer that changes operators into function calls:

u + 2*v   →  op_add( u, op_mul(2, v) )

This transformation alone is quite easy, I just have to find BinaryExpression(A, op, B) and turn it into CallExpression(fn, [A, B]). However I would love to use the type checking of the resulting function. In the handbook you say that:

“Type checking should not happen after transforming. If it does it's more than likely a bug - file an issue!”

Does this mean that my transformed code will still be type-checked as if it were a + b, not op_add(a, b)? Or is it just a misunderstanding on my side?

Thanks!

hey mate! no you're exactly right - it will only type check as if it were a + b - by the time we run our transformers typechecking has already happened and there isn't any opportunity to change it

we can however write type def transformers if you want to change what typedefs are generated

m93a commented

Hey! Thank you so much for your reply – and for the handbook! I'd be lost without it.

These are some unfortunate news. In order for my “modified TypeScript” to be useful at all, I need custom type-checking… This means that transformers can't do the job for me and I'll have to use the Compiler API directly and make TypeScript generate types when I want to, right?

I'll try messing with the compiler and if I manage to do something interesting, I'll let you know 😁️

yeah you basically need to use it directly.. no idea how it will go! if you find anything interesting report back :D

m93a commented

With the help of the StackOverflow community (see my question) I was able to get the compilation working. I had to write a few utility functions that are available in a gist, with them the code looks like this:

import * as ts from "typescript"
import { rewriteNode, rewriteChildren } from "./rewrite";


function compile(fileNames: string[], options: ts.CompilerOptions): void
{
  let program = ts.createProgram(fileNames, options)

  for (let input of program.getSourceFiles())
  if (input.fileName.endsWith('.m.ts'))
    rewriteNode(input, visitor)

  logDiagnostics(ts.getPreEmitDiagnostics(program))

  let emitResult = program.emit()

  logDiagnostics(emitResult.diagnostics)

  let exitCode = emitResult.emitSkipped ? 1 : 0;
  console.log(`Process exiting with code '${exitCode}'.`)
  process.exit(exitCode)
}

const operatorNameMap: { [op in ts.SyntaxKind]?: string } = {
  [ts.SyntaxKind.PlusToken]: 'op_plus',
  [ts.SyntaxKind.MinusToken]: 'op_minus',
  [ts.SyntaxKind.AsteriskToken]: 'op_asterisk',
  [ts.SyntaxKind.SlashToken]: 'op_slash'
}

function visitor(node: ts.Node): ts.Node {
  if (node.kind === ts.SyntaxKind.BinaryExpression) {
    let expr = node as ts.BinaryExpression;
    const name = operatorNameMap[expr.operatorToken.kind]

    if (name !== undefined) {
      const identifier = ts.createIdentifier(name);
      return ts.createCall(identifier, undefined, [expr.left, expr.right]);
    }
  }

  return rewriteChildren(node, visitor);
}

function logDiagnostics(arr: readonly ts.Diagnostic[]) { /* long uninteresting code */ }

compile(['sample.m.ts'], {
  target: ts.ScriptTarget.ES5,
  module: ts.ModuleKind.CommonJS
})

Now I need to hook it up to the language services…