Safe TypeScript Aseem Rastogi

advertisement
Safe TypeScript
Aseem Rastogi
University of Maryland, College Park
End of Internship Talk
Joint work with: Nikhil Swamy (RiSE, Internship mentor),
Cédric Fournet (MSRC),
Gavin Bierman (Oracle),
Panagiotis Vekris (UCSD)
TypeScript
Gradually typed superset of JavaScript (www.typescriptlang.org)
• "Strong tools for large applications"
• "Static checking, symbol-based navigation, statement completion, code refactoring"
• "TypeScript offers classes, modules, and interfaces to help you build robust
components"
• "Compiled into simple JavaScript"
Compared to JavaScript, this is a great leap forward!
But Typing JavaScript is Hard !
For all its dynamic idioms
• TypeScript (like Dart and Closure) gives up soundness, intentionally
• Types are uniformly erased when compiling to JavaScript
• E.g. casts are unchecked
• Unsound type erasure is beneficial
• Lightweight codegen (highly readable JavaScript output)
• Performance identical to plain JavaScript
• Types don’t get in the way of good programmers
• But it also has its disadvantages
• TypeScript components are not robust
(Un-)Robustness of TypeScript Components
Type safety violation
Client:
Client:
function client(Iterator<number> it) {
it["index"] = true;
}
function client(it) {
it["index"] = true;
}
Provider:
Provider:
interface Iterator<A> { next(): A }
var x = { state:[..], index:0,
next() { return state[index++]; }
};
client(x); //client:Iterator<number> => void
var x = { state:[..], index:0,
next() { return state[index++]; }
};
client(x);
TypeScript
JavaScript
TypeScript compiler
(Un-)Robustness of TypeScript Components
Abstraction violation
Client:
Client:
function client(Iterator<number> it) {
(<any> it).index = -1;
}
function client(it) {
it.index = -1;
}
Provider:
Provider:
interface Iterator<A> { next(): A }
var x = { state:[..], index:0,
next() { return state[index++]; }
};
client(x); //client:Iterator<number> => void
var x = { state:[..], index:0,
next() { return state[index++]; }
};
client(x);
TypeScript
JavaScript
TypeScript compiler
Safe TypeScript
• Sound and efficient gradual typing is possible for idiomatic TypeScript
• Sound typing is beneficial
• Finds type errors early
• Found and fixed 478 type errors in TypeScript compiler,
• 1 functional correctness bug in NavierStokes, a heavily tested Octane benchmark
• Provably robust components
• But it also has its cost
• A runtime performance penalty
• Ranging from 6% to 3x on 118,000 lines of code in 8 applications (details follow)
• Need to understand subtle corners of JS semantics and our type system
TypeScript Workflow
app.ts
function f(x):number {
return x.f;
}
function f(x:any):number {
return x.f;
}
tsc app.ts
TypeScript parsing
TypeScript
type inference
app.js
function f(x) {
return x.f;
}
JavaScript emitting
Syntactic errors
Static diagnostic
- basic type errors
Safe TypeScript Workflow
Fully integrated into TypeScript v0.9.5 as an optional compiler flag
app.ts
function f(x):number {
return x.f;
}
function f(x:any):number {
return x.f;
}
function f(x:any):number {
return RT.check(RT.Num,
RT.read(x, "f"));
}
app.js
function f(x) {
return RT.check(RT.Num,
RT.read(x, "f"));
}
tsc --safe app.ts
TypeScript parsing
TypeScript
type inference
Safe TypeScript
type checking
& instrumentation
JavaScript emitting
Syntactic errors
Static diagnostic
- basic type errors
Static diagnostics
- inconsistent subtyping
- implicit downcast from any
- variables not in scope
- unsafe use of this
- projecting methods as fields
Highlights of The Type System
• Object-oriented, with a mixture of structural and nominal types
• Nominal types provide a sound model of JavaScript's semantics of classes
• In contrast: TypeScript is purely structural
• Types are compiled to provide precise run-time type information (RTTI)
• Allows the runtime system to enforce invariants with dynamic checks
• In contrast: RTTI in TypeScript is only what is available in JavaScript
• Selective type-erasure for performance and robustness
• The type system ensures that erasure does not compromise safety
• In contrast: TypeScript uniformly erases all types
By example …
Nominal Classes and Structural Interfaces
interface PointI {
x:number;
y:number
}
class PointC {
constructor(public x:number,
public y:number) { }
}
function f(p:PointC) {
assert(p instanceof PointC);
}
Nominal Classes and Structural Interfaces
interface PointI {
x:number;
y:number
}
class PointC {
constructor(public x:number,
public y:number) { }
}
TypeScript output: leads to runtime error in f
function f(p:PointC) {
assert(p instanceof PointC);
}
Safe TypeScript: Static Type Error
f({x:0, y:0});
{x:number;y:number} is not a subtype of PointC
Nominal Classes and Structural Interfaces
interface PointI {
x:number;
y:number
}
class PointC {
constructor(public x:number,
public y:number) { }
}
function f(p:PointC) {
assert(p instanceof PointC);
}
f(new PointC(0, 0));
Safe TypeScript: OK
Nominal Classes and Structural Interfaces
interface PointI {
x:number;
y:number
}
class PointC {
constructor(public x:number,
public y:number) { }
}
function f(p:PointI) {
return p.x + p.y;
}
Safe TypeScript: OK
f(new PointC(0, 0));
PointC is a subtype of PointI
Highlights of The Type System
• Object-oriented, with a mixture of structural and nominal types
• Nominal types provide a sound model of JavaScript's semantics of classes
• In contrast: TypeScript is purely structural
• Types are compiled to provide precise run-time type information (RTTI)
• Allows the runtime system to enforce invariants with dynamic checks
• In contrast: RTTI in TypeScript is only what is available in JavaScript
• Selective type-erasure for performance and robustness
• The type system ensures that erasure does not compromise safety
• In contrast: TypeScript uniformly erases all types
Tag Objects with RTTI to Lock Invariants
function f(p:any) {
p.x = "boom";
}
function g(p:PointI) {
f(p);
assert(typeof p.x === "number");
}
TypeScript output: leads to runtime error in g
Tag Objects with RTTI to Lock Invariants
shallowTag for structural objects
function f(p:any) {
p.x = "boom";
}
function f(p) { … } //coming up !
function g(p:PointI) {
f(p);
assert(typeof p.x === "number");
}
function g(p) {
f(shallowTag(p, PointI));
…
}
Safe TypeScript: Adds RTTI to objects to lock their type
shallowTag(x,t) = x.tag := combine(x.tag,t); x
Instrumentation of any Code
function f(p:any) {
p.x = "boom";
}
function f(p) {
write(p, "x", "boom"); // fails
}
function g(p:PointI) {
f(p);
assert(typeof p.x === "number");
}
function g(p) {
f(shallowTag(p, PointI));
…
}
Safe TypeScript: Enforces type invariants in any code
write(o,f,v) =
let t = o.rtti;
o[f] = check(v, t[f]);
Tag Objects with RTTI to Lock Invariants
No tagging for class instances
function f(p:any) {
p.x = "boom";
}
function g(p:PointC) {
f(p);
assert(typeof p.x === "number");
}
function g(p) {
f(p); // no tagging
…
}
No tagging for class instances
Class instances have primitive RTTI (prototype chain)
Runtime Checked Downcasts
function f(p:PointI) {
assert(typeof p.x === "number");
}
function g(p:any) {
f(<PointI> p);
}
g({x:"boom",y:0});
TypeScript output: leads to runtime error in f
Runtime Checked Downcasts
Check fields invariants for structural types
function f(p:PointI) {
assert(typeof p.x === "number");
}
function g(p:any) {
f(<PointI> p);
}
g({x:"boom",y:0});
function f(p) { … }
function g(p) {
f(check(p, PointI));
}
…
Safe TypeScript: Checks downcasts at runtime
check(o, PointI) =
if typeof o.x === “number”
&& typeof o.y === “number”
then o.rtti := PointI; o
else die
Runtime Checked Downcasts
Simple instanceof check for class instances
function f(p:PointC) { … }
function f(p) { … }
function g(p:any) {
f(<PointC> p);
}
function g(p) {
f(check(p, PointC));
}
…
g({x:"boom",y:0});
check(o, PointC) =
if o instanceof PointC
then o
else die
Fast instanceof check for class instances
Highlights of The Type System
• Object-oriented, with a mixture of structural and nominal types
• Nominal types provide a sound model of JavaScript's semantics of classes
• In contrast: TypeScript is purely structural
• Types are compiled to provide precise run-time type information (RTTI)
• Allows the runtime system to enforce invariants with dynamic checks
• In contrast: RTTI in TypeScript is only what is available in JavaScript
• Selective type-erasure for performance and robustness
• The type system ensures that erasure does not compromise safety
• In contrast: TypeScript uniformly erases all types
Safe TypeScript adds RTTI Tags On-demand
interface 3dPointI extends PointI {
z:number;
}
function f(r) { … }
function f(r:any) { ... }
function g(q) {
f(shallowTag(q, PointI));
}
function g(q:PointI) {
f(q);
}
function h(p) {
g(shallowTag(p, {z:number}); // diff tagging
}
function h(p:3dPointI) {
g(p);
}
function main(p) { h(p); } // no tagging
function main(p:3dPointI) {
h(p);
}
Safe TypeScript adds minimum RTTI to ensure safety
shallowTag(x, t) = x.rtti := combine(x.rtti, t); x
Programmer-controlled Type Erasure
A new operator on types: "Erased t"
A value of type Erased t is known to be a t statically,
and at runtime it may not have RTTI
Erased types are erased from the JavaScript output
Programmer Controlled Type Erasure
interface PointI extends Erased {
x:number;
y:number
}
interface 3dPointI extends PointI {
z:number;
}
function f(r:any) { ... }
function g(q:PointI) {
f(q);
}
function h(p:3dPointI) {
g(p);
}
function f(r) { ... }
function g(q) {
f(q);
 static type error
}
function h(p) {
g(p);
 compiles as is
}
Cannot pass
erased types
to any context
No tagging
despite loss in
precision
Recall that previously it was:
g(shallowTag(p, {z:number}))
Safe TypeScript: Erased types must only be used statically
Revisiting Robust Components
Client:
function client(Iterator<number> it) {
it["index"] = true; //runtime error
}
Provider:
interface Iterator<A> { next(): A }
Robustness provided by Safe TypeScript
type soundness theorem
Several useful corollaries:
-- RTTI tags are always consistent
-- RTTI tags evolve in subtyping hierarchy
var x = { state:[..], index:0,
next() { return state[index++]; }
};
client(x); //client:Iterator<number> => void
Full formalization and proofs in technical report: http://research.microsoft.com/apps/pubs/default.aspx?id=224900
Revisiting Robust Components
Provider’s perspective
Client:
function client(Iterator<number> it) {
(<any> it).index = -1;
}
Provider:
interface Iterator<A> { next(): A }
var x = { state:[..], index:0,
next() { return state[index++]; }
};
client(x); //client:Iterator<number> => void
Revisiting Robust Components
Provider’s perspective
Client:
function client(Iterator<number> it) {
//static error
(<any> it).index = -1;
}
Stronger abstraction using erased types
Safe TypeScript provides an abstraction
theorem for erased types
Provider:
interface Iterator<A> extends Erased { next(): A }
var x = { state:[..], index:0,
next() { return state[index++]; }
};
client(x); //client:Iterator<number> => void
Full formalization and proofs in technical report: http://research.microsoft.com/apps/pubs/default.aspx?id=224900
Much more …
•
•
•
•
•
•
•
•
•
•
•
•
Arrays (with mutability controls)
Dictionaries
Inheritance
Overloading
Generics with bounded polymorphism
Optional fields/arguments/variadic functions
Auto-boxing
Primitive prototype hierarchy
Closed vs. open records
Nominal interfaces
Enums
…
All these features allow us to handle practical TypeScript developments …
Experience with SafeTypeScript
Bootstrapping Safe TypeScript compiler (implemented in TypeScript v0.9.5)
• 90,000 lines of code (80,000 lines of TypeScript compiler)
• Heavily class based, most of the code is carefully type annotated
• Static errors
• 478 in total
•
•
•
•
•
98 uses of bi-variant array subtyping
130 uses of covariant method argument subtyping
128 cases of variable scoping issues
52 cases of projecting a method (leading to potential unsound use of this parameter)
…
• Runtime errors
• 26 failed downcasts
• 5 in our own code !
15% runtime overhead of type safety
Experience with SafeTypeScript
Compiling TypeScript v1.1
• 18,000 lines of code
• Heavily interface based
• Static errors
• 81 in total
• Mainly variable scoping and array subtyping
3x runtime overhead of type safety
High overhead because of more structural types
Have not optimized Safe TypeScript runtime for structural types
Experience with SafeTypeScript
Octane benchmarks
• 10,000 lines of code
• Static errors
• Found 1 variable scoping bug in heavily tested NavierStokes
• High runtime overhead when no annotations
• 2.4x (Splay) to 72x (Crypto), Average: 22x
• Performance recovers once we add type annotations
• Average overhead: 6.5%
Demo
(
Examples on Online Playground:
http://research.microsoft.com/en-us/um/people/nswamy/Playground/TSSafe/
)
Limitations and Work in Progress
• eval and friends
• Adversarial typing of unsafe constructs – Swamy et. al. POPL'14
• Implementation limitations:
• Does not support external modules
• Current implementation is in TypeScript v0.9.5 that has evolved to v1.1
• Ongoing discussion about integrating Safe TypeScript in TypeScript
v1.1
Safe TypeScript
Sound and efficient gradual type system for TypeScript
Download:
http://research.microsoft.com/en-us/downloads/b250c887-2b79-4413-9d7a5a5a0c38cc57/
Submitted POPL'15 paper:
http://www.cs.umd.edu/~aseem/safets.pdf
Technical report (with full formalization and proofs):
http://research.microsoft.com/apps/pubs/default.aspx?id=224900
Online playground:
http://research.microsoft.com/en-us/um/people/nswamy/Playground/TSSafe/
Structural types distinguish fields from methods
Handling this soundly
class Line {
constructor(public p1:Point,
public p2:Point){}
public moveUp() {
this.p1.y++; this.p2.y++;
}
}
function g(l:{moveUp:() => void}) {
var f = l.moveUp;
f();
}
function h(p:Point) {
g(new Line(p, p));
}
Compiles without warnings in TypeScript
• Classes are convertible with their structure
Line <: {moveUp() :void;
p1:Point;
p2:Point}
//method
//field
//field
window.p1 is undefined, so p1.y crashes
SafeTypeScript
• Line does not contain a field called moveUp
• Only a method called moveUp
Structural types distinguish fields from methods
Handling this soundly
class Line {
constructor(public p1:Point,
public p2:Point){}
public moveUp() {
this.p1.y++; this.p2.y++;
}
}
Compiles without warnings in TypeScript
• Classes are convertible with their structure
function g(l:{moveUp(): void}) {
var f = l.moveUp;
f();
}
SafeTypeScript
• Cannot project a method
function h(p:Point) {
g(new Line(p, p));
}
Line <: {moveUp() :void;
p1:Point;
p2:Point}
//method
//field
//field
Structural types distinguish fields from methods
Handling this soundly
class Line {
constructor(public p1:Point,
public p2:Point){}
public moveUp() {
this.p1.y++; this.p2.y++;
}
}
function g(l:{moveUp(): void}) {
l.moveUp();
}
function h(p:Point) {
g(new Line(p, p));
g({moveUp()
},
g({moveUp : {
()this.p1.y++;
=> {this.p1.y++;},
p1:p,
p1:p,
p2:p});
p2:p})
}
Compiles without warnings in TypeScript
• Classes are convertible with their structure
Line <: {moveUp() :void;
p1:Point;
p2:Point}
//method
//field
//field
SafeTypeScript: Ok!
Object
literal
witha amethod
method
SafeTypeScript: A
function
is not
Field Addition and Deletion
Dynamically typed unless it becomes static
function f(p:PointI) {
p["z"] = 0;
delete p.z;
}
function f(p: PointI) {
write(p, "z", 0);
delete(p, "z");
}
SafeTypeScript: Both write and delete succeed at runtime
Field Addition and Deletion
Dynamically typed unless it becomes static
function g(p:3dPointI) { … }
function g(p:3dPointI) { … }
function f(p:PointI) {
p["z"] = 0;
g(<3dPointI> p);
delete p.z;
}
function f(p:PointI) {
write(p, "z", 0);
g(check(p, 3dPointI));
delete(p, "z");
}
SafeTypeScript: write succeeds at runtime
SafeTypeScript: check succeeds at runtime
SafeTypeScript: delete fails at runtime – violates invariant of g
Download