TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Various bugs with class expressions and JavaScript decorators (code run twice, internal compiler crash)

Open evanw opened this issue 1 year ago • 1 comments

🔎 Search Terms

javascript decorators proposal class expressions crash debug failure error

🕗 Version & Regression Information

  • This is a crash

⏯ Playground Link

No response

💻 Code

Case 1: (playground link)

const log: string[] = []
const dec = (x: number, y: number, z: number, t: number) => {
  log.push('x' + x)
  return (_: any, ctx: DecoratorContext): any => {
    log.push('y' + y)
    ctx.addInitializer(() => {
      log.push('z' + z)
    })
    if (ctx.kind === 'field') return () => { log.push('f' + t) }
    if (ctx.kind === 'accessor') return { init: () => { log.push('a' + t) } }
  }
}
const Foo = @dec(0, 4, 2, -1) class {
  @dec(1, 3, 3, 1) field: undefined
  @dec(2, 2, 0, 0) static field: undefined
  @dec(3, 1, 4, 1) accessor accessor: undefined
  @dec(4, 0, 1, 0) static accessor accessor: undefined
}
new Foo
const expected = 'x0,x1,x2,x3,x4,y0,y1,y2,y3,y4,f0,z0,a0,z1,z2,f1,z3,a1,z4'
const success = log + '' === expected
console.log('success: ' + success)
if (!success) {
  console.log('observed: ' + log)
  console.log('expected: ' + expected)
}

Case 2 (playground link):

This is case 1 with the decorator on the class expression removed.

const dec = (x: number, y: number, z: number, t: number) => {
  return (_: any, ctx: DecoratorContext): any => {
  }
}
const Foo = class {
  @dec(1, 3, 3, 1) field: undefined
  @dec(2, 2, 0, 0) static field: undefined
  @dec(3, 1, 4, 1) accessor accessor: undefined
  @dec(4, 0, 1, 0) static accessor accessor: undefined
}

Case 3 (playground link):

This is case 1 with the two instance elements removed.

const dec = (x: number, y: number, z: number, t: number) => {
  return (_: any, ctx: DecoratorContext): any => {
  }
}
const Foo = @dec(0, 4, 2, -1) class {
  @dec(2, 2, 0, 0) static field: undefined
  @dec(4, 0, 1, 0) static accessor accessor: undefined
}

🙁 Actual behavior

Case 1:

When run, this prints the following:

[LOG]: "success: false" 
[LOG]: "observed: x0,x1,x2,x3,x4,y0,y1,y2,y3,y4,f0,z0,a0,z1,z2,f0,z0,a0,f1,z3,a1,z4" 
[LOG]: "expected: x0,x1,x2,x3,x4,y0,y1,y2,y3,y4,f0,z0,a0,z1,z2,f1,z3,a1,z4" 

That means:

  1. TypeScript's behavior for decorators on class expressions diverges from TypeScript's behavior for class statements. The expected output was taken from TypeScript's transformation of the same code but as a class statement instead of a class expression, and appears to be correct.

  2. The code that TypeScript generates evaluates several initializers multiple times. Specifically, static field and static accessor initializers are run twice, which corresponds to duplicated calls to __runInitializers(_classThis, _static_field_extraInitializers) and __runInitializers(_classThis, _static_accessor_initializers) in the generated code.

Case 2:

This crashes the playground compiler with Error: Debug Failure. and a minified stack trace.

Case 3:

This crashes the playground compiler with Error: Debug Failure. False expression: Undeclared private name for property declaration. and a minified stack trace.

🙂 Expected behavior

TypeScript should transform decorators into code that behaves the same whether or not the original class was an expression or a statement. The compiler should also not crash with debug assertion failures.

For context, I discovered these issues while expanding the coverage of my decorator test suite: https://github.com/evanw/decorator-tests. I intend to use these test cases when adding support for transforming decorators to esbuild.

Additional information about the issue

No response

evanw avatar May 04 '24 19:05 evanw

For Case 1, it looks like the static field is being emit twice, which is triggering initializers twice.

For Case 2 and Case 3, it looks like a combination of target: esnext and useDefineForClassFields: false playing poorly with decorators.

rbuckton avatar May 08 '24 02:05 rbuckton