-
Notifications
You must be signed in to change notification settings - Fork 14
Improve declaration emit type safety. #124
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
Improve declaration emit type safety. #124
Conversation
9b72589
to
ad7d11e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love these changes
@@ -377,7 +377,7 @@ function visitArrayWorker( | |||
* Starts a new lexical environment and visits a statement list, ending the lexical environment | |||
* and merging hoisted declarations upon completion. | |||
*/ | |||
export function visitLexicalEnvironment(statements: NodeArray<Statement>, visitor: Visitor, context: TransformationContext, start?: number, ensureUseStrict?: boolean, nodesVisitor: NodesVisitor = visitNodes) { | |||
export function visitLexicalEnvironment(statements: NodeArray<Statement>, visitor: Visitor, context: CoreTransformationContext, start?: number, ensureUseStrict?: boolean, nodesVisitor: NodesVisitor = visitNodes) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rbuckton Do you have any thoughts on this? It doesn't look like any of these visitors actually need the entire transformation context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @rbuckton, @jakebailey, we have some further changes that depend on these, could you please take a look?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think anything actually uses CoreTransformationContext
anymore (chainBundle
does, but currently its only ever used in places where a full transformation context is available). I'd originally added it as part of the node factory API refactor in microsoft#35282, with the intent to leverage it in microsoft#39084, but ended up not needing it. It might be better to just do away with CoreTransformationContext
entirely, and just have any customized TransformationContext
impelementations patch in unsupported methods from nullTransformationContext
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nullTransformationContext
throw exceptions for some of the methods:
{
getEmitResolver: notImplemented,
getEmitHost: notImplemented,
getEmitHelperFactory: notImplemented,
//....
}
My problem is that this is not exactly safe. There are no guarantees that those methods will not get called.
This is especially problematic now in the declaration transform, as this will contain both paths that rely on getEmitHost
and getEmitResolver
(the regular TSC declaration emit) but also paths that should not call these (when called from transformaDeclaration
). I would like to make sure that it's not possible to mistakenly depend on emitHost and emitResolver if they are not present.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You would get an error if you tried to depend on them and they didn't exist, though I can see your point about leveraging the lack of these methods to ensure you don't accidentally reach for a capability that isn't present. I'm mildly concerned that having too many versions of TransformationContext
will have the potential to impact performance if the same functions in the declaration transformer witnesses different object shapes and ends up with polymorphic ICs. If there is a performance issue, it likely won't show up in our normal benchmarks since they're unlikely to exercise code paths that would trigger such an IC transition. It will be something to keep an eye on, though.
I'm fine with switching these to CoreTransformationContext
.
ad7d11e
to
f6f65e1
Compare
1ea970e
to
f9aa1b6
Compare
} from "../../_namespaces/ts"; | ||
|
||
/** @internal */ | ||
export interface IsolatedTransformationContext extends CoreTransformationContext { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this need its own version of TransformationContext
if both are marked @internal
? TransformationContext
already has getEmitResolver()
, getCompilerOptions()
, factory
, and addDiagnostic
. If the only reason is the useLocalInferenceTypePrint
field, then I would argue that it doesn't belong on the context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes but IsolatedTransformationContext
provides an emit resolver that has fewer features than the full resolver (just the CoreEmitResolver
). I would ideally want this to be obvious in the declaration transform.
We could add implementations that just throw for everything that is not supported, but that would be hard to follow and prone to mistakes in the declaration transform (someone might accidentally call one of these un-implemented methods)
Right now we have a discriminated union in declaration emit that ensures you need to check if what you actually have available before you use it (useLocalInferenceTypePrint
and isolatedDeclarations
both act as discriminants)
const { localInferenceResolver, isolatedDeclarations, host, resolver, symbolTracker, ensureNoInitializer, useLocalInferenceTypePrint } = createTransformerServices(); |
getEmitHost() { | ||
return emitHost; | ||
}, | ||
useLocalInferenceTypePrint: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem like it belongs on a TransformationContext. Can you provide some additional context on what this field implies and why it belongs here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently the declaration transform can either use the full checker type printer or it can use a new simpler printer that infers information only from the local declaration.
Initially I considered using the minimal type printer always when isolatedDeclarations
is enabled. @DanielRosenwasser suggested however that the new minimal type printer should not be used unless we are in transpileDeclaration
which is the only context in which the checker might actually not exist. This useLocalInferenceTypePrint
is the way to turn on or off the usage of the minimal type printer.
We could also put it as an extra optional parameter on transformDeclarations
that switches this on. Or we can add it to CompilerOptions
(as an internal property). Adding it directly to the context seemed nicer as it acts as a discriminant on the union of TransformationContext | IsolatedDeclarationContext
@@ -5665,8 +5665,28 @@ export enum TypeReferenceSerializationKind { | |||
ObjectType, | |||
} | |||
|
|||
|
|||
/** @internal */ | |||
export interface CoreEmitResolver { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If both CoreEmitResolver
and EmitResolver
are internal, why do we need two interfaces? Is it just to allocate a special EmitResolver
that doesn't implement the whole interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes exactly. From transformDeclaration
we don't provide the whole resolver, only a small subset that can be reasonably implemented without the full type checker.
@@ -377,7 +377,7 @@ function visitArrayWorker( | |||
* Starts a new lexical environment and visits a statement list, ending the lexical environment | |||
* and merging hoisted declarations upon completion. | |||
*/ | |||
export function visitLexicalEnvironment(statements: NodeArray<Statement>, visitor: Visitor, context: TransformationContext, start?: number, ensureUseStrict?: boolean, nodesVisitor: NodesVisitor = visitNodes) { | |||
export function visitLexicalEnvironment(statements: NodeArray<Statement>, visitor: Visitor, context: CoreTransformationContext, start?: number, ensureUseStrict?: boolean, nodesVisitor: NodesVisitor = visitNodes) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think anything actually uses CoreTransformationContext
anymore (chainBundle
does, but currently its only ever used in places where a full transformation context is available). I'd originally added it as part of the node factory API refactor in microsoft#35282, with the intent to leverage it in microsoft#39084, but ended up not needing it. It might be better to just do away with CoreTransformationContext
entirely, and just have any customized TransformationContext
impelementations patch in unsupported methods from nullTransformationContext
.
f9aa1b6
to
b7d3c82
Compare
Signed-off-by: Titian Cernicova-Dragomir <[email protected]>
b7d3c82
to
211b056
Compare
Signed-off-by: Titian Cernicova-Dragomir <[email protected]>
afc0fa2
to
97161e0
Compare
src/compiler/transformer.ts
Outdated
onEmitNode: noEmitNotification, | ||
addDiagnostic: noop, | ||
}; | ||
export function createTransformationContext(kind: TransformationContextKind.NullContext): NullTransformationContext; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The overload seems a bit overcomplicated since we'll only ever create one nullTransformationContext
. You could just leave nullTransformationContext
as is with the exception of giving it the type NullTransformationContext
and adding the kind
, and then just use return { ...nullTransformationContext, kind: TransformationContext.IsolatedContext, <whatever overrides you want> }
. I realize that's pretty much what you had before, but I think kind
is actually a bit clearer than the boolean field you were using previously.
db2e23c
to
a2fab36
Compare
Signed-off-by: Titian Cernicova-Dragomir <[email protected]>
a2fab36
to
c45ffaf
Compare
factory: factory, // eslint-disable-line object-shorthand | ||
getCompilerOptions: () => ({}), | ||
getEmitResolver: notImplemented, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing these results in a different shape than a normal transformation context, which is observable to the runtime during normal compilation since the null context is used by the command line compiler. I'd prefer we keep these in for the null context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since some of the removed members throw an exception at runtime, I would not expose them on the NullTransformationContext
type. If someone somewhere is actually using the nullTransformationContext
as a full transformation context I kept the nullTransformationContext
to have the same runtime members but hid it behind the more restrictive NullTransformationContext
Or did you have in mind that the NullTransformationContext
type is more similar to a full TransformationContext
. I don't particularly like that idea since some of them throw runtime exceptions so ideally the type system should not let you access those members.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NullTransformationContext
has always just been a normal TransformationContext
with the features that depended on a type checker or program disabled. Having the shape remain stable avoids V8 recompiling large chunks of tsc.js because the checker sees one shape when it creates synthetic nodes when producing diagnostics via typeToTypeNode
and another shape when it sees a full checker when doing emit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't particularly like that idea since some of them throw runtime exceptions so ideally the type system should not let you access those members.
If we want the type to follow the shape but limit access, we could always define a NullTransformationContext
type that makes the return types of those methods never
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rbuckton I changed NullTransformationContext
to have the same shape as TransformationContext
(just the not implemented methods return never)
Signed-off-by: Titian Cernicova-Dragomir <[email protected]>
56098f3
to
a3a5049
Compare
…ion context. Signed-off-by: Titian Cernicova-Dragomir <[email protected]>
e2f7da9
to
202d522
Compare
No description provided.