Repository navigation
Description
TypeScript Version: 2.0.2
Code
Unfortunately, I have not been able to construct a reduced testcase, so I prepared a branch in the failing project to demonstrate the issue. Please checkout
https://github.com/DirtyHairy/6502.ts
and switch to the branch ts-broken. Doing npm install and grunt initial will demonstrate the issue. After that, the failing compile can be retriggered via grunt ts.
Expected behavior:
Project builds.
Actual behavior:
Using tsc v2.0.2
src/machine/stella/Board.ts(139,16): error TS2365: Operator '!==' cannot be applied to types 'ExecutionState.boot' and 'ExecutionState.fetch'
The code used to compile fine before 2.0.2 . The failing code fragment is this function:
boot(): Board {
let cycles = 0,
cpuCycles = 0;
this.reset();
if (this._cpu.executionState !== CpuInterface.ExecutionState.boot)
throw new Error("Already booted!");
while (this._cpu.executionState !== CpuInterface.ExecutionState.fetch) {
this._cycle();
cycles++;
if (this._subClock === 0) {
cpuCycles++;
}
}
this.cpuClock.dispatch(cpuCycles);
this.clock.dispatch(cycles);
return this;
}
The corresponding types are declared in this snippet:
interface CpuInterface {
// [...]
executionState: CpuInterface.ExecutionState;
// [...]
}
module CpuInterface {
export const enum ExecutionState {
boot, fetch, execute
}
// [...]
}
I think the problem was introduced when enum values were split into different types, and I suspect that two issues are at work here:
- The assertion leads the compiler to infer the type of
this._cpu.executionStateto beCpuInterface.ExecutionState.boot. However, callingthis.cycle()will mutate the execution state. - The compiler deems the two sides of
!==to be of different type and rejects the comparision.
After introducing a manual cast
while (this._cpu.executionState as CpuInterface.ExecutionState !== CpuInterface.ExecutionState.fetch) {
the code compiles fine
However, as I said, I have not been able to construct a reduced test case, so some other aspect of my code seems to contribute to the compiler's confusion 😏
Activity
sandersn commentedon Sep 14, 2016
It looks like control flow is tripping you up, although I'm not sure yet whether if it's working as intended. Basically, after the line
if (this._cpu.executionState !== CpuInterface.ExecutionState.boot)throws an error, control flow analysis narrowsexecutionStatecan't be anything butboot. So the comparison at the beginning of thewhileis always false.I know that
whileloops are more complicated thanifstatements in that they try to understand any type guards inside the loop body. But I don't think thatthis._cycle()is recognized as something that could alterthis._cpu. I'll have to confirm by checking the code, though.If that's the case, then perhaps control flow analysis should know when it is narrowing a property of
thisand understand that method calls onthiscan also change the value of the property.sandersn commentedon Sep 14, 2016
Yes, I'm pretty sure that this feature is working as designed for now. I observed that adding an obvious narrowing inside the body of the loop also gets rid of the error, like this:
Since
this._cycle(): void, theifbody never even executes, but control flow analysis doesn't catch that, so it addsfetchback to the list of states thatexecutionStatecould have.For a real workaround, I'd recommend not breaking the
CpuInterfaceabstraction and instead adding methods likeCpuInterface.hasBootedandCpuInterface.isFetchingand using those instead of directly checking the contents ofthis._cpu.executionState.mhegazy commentedon Sep 14, 2016
This looks like the same issue as #10613. I have a similar explanation in #10613 (comment)
DirtyHairy commentedon Sep 14, 2016
Hi Nathan Shively-Sanders (@sandersn) !
Thanks for the feedback! As this is a performance critical part of the emulator (the very inner loop, executed at ~3-4 MHz), I prefer checking the state via direct comparision over a function call at the price of loosening the abstraction. However, I am fine with either of the two other workarounds ;)
Is this part of control flow analysis going to be refined in a future release? The way JS and TS are designed, any function call might have side effects that mutate this._cpu, so the assumptions made by the compiler in this situation seem rather brittle to me ;)
Cheers
-Christian
sandersn commentedon Sep 14, 2016
Yes, control flow is still being improved as we observe the ways it fails. Lots of tweaks and fixes are going into 2.1 already, for example.