Skip to content

Commit 1795f6f

Browse files
committed
feat: add type narrow to support better type check
1 parent d559982 commit 1795f6f

File tree

10 files changed

+3969
-1
lines changed

10 files changed

+3969
-1
lines changed

src/compiler.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ import {
8888
PropertyPrototype,
8989
IndexSignature,
9090
File,
91-
mangleInternalName
91+
mangleInternalName,
92+
TypedElement
9293
} from "./program";
9394

9495
import {
@@ -210,6 +211,7 @@ import {
210211
import {
211212
ShadowStackPass
212213
} from "./passes/shadowstack";
214+
import { TypeNarrowInfo } from "./conditionInfo";
213215

214216
/** Compiler options. */
215217
export class Options {
@@ -2665,12 +2667,17 @@ export class Compiler extends DiagnosticEmitter {
26652667
// ) └┬─────────┴─┘
26662668
// ... ┌◄┘
26672669

2670+
this.currentFlow.startCompileCondition();
2671+
26682672
// Precompute the condition (always executes)
26692673
var condExpr = this.makeIsTrueish(
26702674
this.compileExpression(statement.condition, Type.bool),
26712675
this.currentType,
26722676
statement.condition
26732677
);
2678+
2679+
const conditionInfoContainer = this.currentFlow.stopCompileCondition();
2680+
26742681
var condKind = this.evaluateCondition(condExpr);
26752682

26762683
// Shortcut if the condition is constant
@@ -2700,11 +2707,13 @@ export class Compiler extends DiagnosticEmitter {
27002707
var thenFlow = flow.fork();
27012708
this.currentFlow = thenFlow;
27022709
thenFlow.inheritNonnullIfTrue(condExpr);
2710+
conditionInfoContainer.trueInfo.forEach((info) => info.apply());
27032711
if (ifTrue.kind == NodeKind.BLOCK) {
27042712
this.compileStatements((<BlockStatement>ifTrue).statements, false, thenStmts);
27052713
} else {
27062714
thenStmts.push(this.compileStatement(ifTrue));
27072715
}
2716+
conditionInfoContainer.trueInfo.forEach((info) => info.recover());
27082717
var thenTerminates = thenFlow.isAny(FlowFlags.TERMINATES | FlowFlags.BREAKS);
27092718
if (thenTerminates) {
27102719
thenStmts.push(module.unreachable());
@@ -2718,11 +2727,13 @@ export class Compiler extends DiagnosticEmitter {
27182727
let elseFlow = flow.fork();
27192728
this.currentFlow = elseFlow;
27202729
elseFlow.inheritNonnullIfFalse(condExpr);
2730+
conditionInfoContainer.falseInfo.forEach((info) => info.apply());
27212731
if (ifFalse.kind == NodeKind.BLOCK) {
27222732
this.compileStatements((<BlockStatement>ifFalse).statements, false, elseStmts);
27232733
} else {
27242734
elseStmts.push(this.compileStatement(ifFalse));
27252735
}
2736+
conditionInfoContainer.falseInfo.forEach((info) => info.recover());
27262737
let elseTerminates = elseFlow.isAny(FlowFlags.TERMINATES | FlowFlags.BREAKS);
27272738
if (elseTerminates) {
27282739
elseStmts.push(module.unreachable());
@@ -5846,6 +5857,7 @@ export class Compiler extends DiagnosticEmitter {
58465857
var flow = this.currentFlow;
58475858
var target = resolver.lookupExpression(expression, flow); // reports
58485859
if (!target) return this.module.unreachable();
5860+
if (target instanceof TypedElement) (<TypedElement>target).recoverType();
58495861
var thisExpression = resolver.currentThisExpression;
58505862
var elementExpression = resolver.currentElementExpression;
58515863

@@ -7903,6 +7915,14 @@ export class Compiler extends DiagnosticEmitter {
79037915
this.currentType = Type.bool;
79047916
return this.module.unreachable();
79057917
}
7918+
if (expression.expression.kind == NodeKind.IDENTIFIER) {
7919+
const identifier = <IdentifierExpression>expression.expression;
7920+
const element = flow.lookup(identifier.text);
7921+
if (element && element instanceof TypedElement) {
7922+
const typedElement = <TypedElement>element;
7923+
flow.addConditionInfo(new TypeNarrowInfo(typedElement, expectedType));
7924+
}
7925+
}
79067926
return this.makeInstanceofType(expression, expectedType);
79077927
}
79087928

src/conditionInfo.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { TypedElement } from "./program";
2+
import { Type } from "./types";
3+
4+
export class ConditionInfoContainer {
5+
trueInfo: ConditionInfo[] = [];
6+
falseInfo: ConditionInfo[] = [];
7+
8+
switchTrueAndFalse(): void {
9+
let tmp = this.trueInfo;
10+
this.trueInfo = this.falseInfo
11+
this.falseInfo = tmp
12+
}
13+
}
14+
15+
export class ConditionInfo {
16+
apply(): void {}
17+
recover(): void {}
18+
}
19+
20+
export class TypeNarrowInfo extends ConditionInfo {
21+
constructor(public typedElement: TypedElement, public narrowedType: Type) {
22+
super();
23+
}
24+
25+
override apply(): void {
26+
this.typedElement.narrowType(this.narrowedType);
27+
}
28+
override recover(): void {
29+
this.typedElement.recoverType();
30+
}
31+
}

src/flow.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import {
9090
import {
9191
BuiltinNames
9292
} from "./builtins";
93+
import { ConditionInfo, ConditionInfoContainer } from "./conditionInfo";
9394

9495
/** Control flow flags indicating specific conditions. */
9596
export const enum FlowFlags {
@@ -1221,6 +1222,25 @@ export class Flow {
12211222
}
12221223
}
12231224

1225+
conditionInfoStack: ConditionInfoContainer[] = []
1226+
startCompileCondition(): void {
1227+
this.conditionInfoStack.push(new ConditionInfoContainer());
1228+
}
1229+
stopCompileCondition(): ConditionInfoContainer {
1230+
return assert(this.conditionInfoStack.pop());
1231+
}
1232+
addConditionInfo(conditionInfo: ConditionInfo, branch: bool = true): void {
1233+
if (this.conditionInfoStack.length > 0) {
1234+
const infoContainer =
1235+
this.conditionInfoStack[this.conditionInfoStack.length - 1];
1236+
if (branch) {
1237+
infoContainer.trueInfo.push(conditionInfo);
1238+
} else {
1239+
infoContainer.falseInfo.push(conditionInfo);
1240+
}
1241+
}
1242+
}
1243+
12241244
/**
12251245
* Tests if an expression can possibly overflow in the context of this flow. Assumes that the
12261246
* expression might already have overflown and returns `false` only if the operation neglects

src/program.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2992,6 +2992,7 @@ export abstract class TypedElement extends DeclaredElement {
29922992

29932993
/** Resolved type. Set once `is(RESOLVED)`, otherwise void. */
29942994
type: Type = Type.void;
2995+
typeStack: Type[] = [];
29952996

29962997
constructor(
29972998
/** Specific element kind. */
@@ -3017,6 +3018,15 @@ export abstract class TypedElement extends DeclaredElement {
30173018
this.type = type;
30183019
this.set(CommonFlags.RESOLVED);
30193020
}
3021+
narrowType(type: Type): void {
3022+
if (type.isClass) {
3023+
this.typeStack.push(this.type);
3024+
this.type = type;
3025+
}
3026+
}
3027+
recoverType(): void {
3028+
this.type = this.typeStack.pop() || this.type;
3029+
}
30203030
}
30213031

30223032
/** A file representing the implicit top-level namespace of a source. */

tests/compiler/typenarrow-error.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"asc_flags": [
3+
],
4+
"stderr": [
5+
"TS2339: Property 'foo' does not exist on type 'typenarrow-error/A",
6+
"EOF"
7+
]
8+
}

tests/compiler/typenarrow-error.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class A {}
2+
class B extends A {
3+
foo(): void {}
4+
}
5+
6+
let t = new A();
7+
if (t instanceof B) {
8+
t.foo();
9+
t = new A();
10+
t.foo();
11+
}
12+
13+
ERROR("EOF");

0 commit comments

Comments
 (0)