Example Angular Usage and Overview of Compiler Implementation
@haskellandchill
@publicismedia
You need to model a UI element where the User can not make a selection, select a specific object, or select "all objects".
Then you need to take an action based on what the User has done. What if you miss a case?
interface NoneSelected {kind: "NoneSelected"};
interface AllSelected {kind: "AllSelected"};
interface ObjectSelected {kind: "ObjectSelected", object: Object};
type Selection = NoneSelected | AllSelected | ObjectSelected;
let none: Selection = {kind: "NoneSelected"};
let all: Selection = {kind: "AllSelected"};
let object: Selection = {kind: "ObjectSelected", object: object};
switch(this.selection.kind) {
case "NoneSelected": return "maroon";
case "AllSelected": return "teal";
case "ObjectSelected": return this.selection.object.color;
default: impossible(this.selection);
}
function impossible(x: never): never { throw new Error(); };
Read Research Papers The Code!
Create AST of switch syntax
Store switch flow constraints by binding over the AST
Check switch's flow constraints against its consumer's and components' flow constraints
interface SwitchStatement extends Statement {
kind: SyntaxKind.SwitchStatement;
expression: Expression;
caseBlock: CaseBlock;
}
interface CaseBlock extends Node {
kind: SyntaxKind.CaseBlock;
parent?: SwitchStatement;
clauses: NodeArray<CaseOrDefaultClause>;
}
interface CaseClause extends Node {
kind: SyntaxKind.CaseClause;
parent?: CaseBlock;
expression: Expression;
statements: NodeArray<Statement>;
}
interface DefaultClause extends Node {
kind: SyntaxKind.DefaultClause;
parent?: CaseBlock;
statements: NodeArray<Statement>;
}
The original implementation in the TypeScript compiler was syntax directed, following the shape of the AST. This means for example, that a node (which should be a type guard of some kind) in the AST can only narrow the type of its children.
Setting a type to a more specific type than its declared type is called narrowing or refining.
Refining types using type guards in TypeScript |
The control flow graph can be implemented by giving each node of the AST an adjacency list containing the prior nodes in the control flow.
type FlowNode = ... | FlowSwitchClause | ...;
interface FlowSwitchClause extends FlowNodeBase {
switchStatement: SwitchStatement;
antecedent: FlowNode;
}
The compiler performs a bind step in a single pass over the AST setting the scopes of all variables, and reporting errors for unreachable code.
To avoid confusion, a node from the AST is called a node and a node from the control flow graph a flow node.
function checkApplicableSignature(
node: CallLikeExpression,
args: ReadonlyArray<Expression>,
signature: Signature
) {
const thisType = getThisTypeOfSignature(signature);
if (thisType && thisType !== voidType && node.kind !== SyntaxKind.NewExpression) {}
const headMessage = Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1;
const argCount = getEffectiveArgumentCount(node, args, signature);
for (let i = 0; i < argCount; i++) {}
}
"Argument of type '{0}' is not assignable to parameter of type '{1}'."