Nested Refinement Types for Dynamic Languages
This paper investigates the nuances of dynamic languages, exploring their untyped nature and core features such as reflective type tests, dictionary-style objects, and duck typing. Furthermore, it examines the implications for reliability and performance in the context of rapid prototyping and large applications prominent on the web today. By integrating refinement types via tag-tests and dictionaries, it aims to enhance type checking while addressing key challenges. The research informs on the evolution of JavaScript and introduces approaches that elevate its type system.
Nested Refinement Types for Dynamic Languages
E N D
Presentation Transcript
Nested Refinement Typesfor Dynamic Languages Ravi Chugh
What are “Dynamic” Languages? Untyped, and characterized by common features • Reflective type-tests typeof x == ‘number’ • Dictionary-style objects obj[key] = val • “Duck typing” if(duck.quack){ duck.quack()} • Dynamic code generation eval(‘var x = 1’) • Rich string primitives arr.join(‘,’)
Why Study Dynamic Languages? http://stackoverflow.com/tags 11-11-11
Why Study Dynamic Languages? Pervasive, thanks to the web • No static checking rapid prototyping • String libraries + code genmulti-language systems Important, thanks to the web • Large applications written in these languages • Reliability matters: security, availability, extensibility • Performance matters: the new Browser Wars • No static checking reliability and performance is hard JavaScript is at the heart of this
Isn’t JavaScript a Terrible Language? undefined value scope manipulation primitive typemanipulation implicit updates toglobal object {} typeofx := 1super implicit coercion misleading syntax eval() weird comparasions NaN != NaN Infinity == Infinity ‘,,,’ == new Array(4) implicitvar lifting
Isn’t JavaScript Awesome?! Core consists of bread-and-butter features {} typeofx := 1super These features not going away, nor should they* Found in many languages, typed and untyped Lambdas in a mainstream language! • Core features are difficult good for research • Core features are expressive good for practice JavaScript is evolving
Research Plan System D lambdas, dictionaries, tag-tests + explicit references + prototype-based inheritance Dependent JavaScript translates to D++ Applications in DJS DJS {} typeofx := 1super + prototypes + references D
Research Plan System D lambdas, dictionaries, tag-tests + explicit references + prototype-based inheritance Dependent JavaScript translates to D++ Applications in DJS DJS {} typeofx := 1super + prototypes + references D
Research Plan System D lambdas, dictionaries, tag-tests + explicit references + prototype-based inheritance Dependent JavaScript translates to D++ Applications in DJS DJS {} typeofx := 1super + prototypes + references D
Motivating Example tag-tests affect control flow iftagoff=“Str”thend.n + d[f](0) elsed.n + f(0) dictionaries indexed by string literals or arbitrary values first-class functions can appear inside dictionaries
Approach: Refinement Types tag-tests Type environment tracks control flow predicates x :: {|tag()=“Int”tag()=“Bool”} x :: {|tag()=“Bool”} x :: {|tag()=“Int”} iftagofx=“Int”then 0 - xelse notx
Approach: Refinement Types tag-tests d.n + d[m] dictionaries {|tag()=“Dict” tag(sel(,“n”))=“Int” tag(sel(,m))=“Int”} d :: McCarthy axioms
Approach: Refinement Types tag-tests 1 +f(0) dictionaries first-class functions x.x :: f :: x:{|tag()=“Int”}{|tag()=“Int”} x:{|tag()=“Int”}{|n=x} Clear separation of base and arrow types T ::= {|p} | x:T1T2
Approach: Refinement Types tag-tests 1 +f(0) dictionaries first-class functions
Approach: Refinement Types tag-tests 1 + d[f](0) dictionaries d :: {|tag()=“Dict” sel(,f) ??? } first-class functions Key Challenges How to describe arrow inside formula? How to keep type checking decidable?
Approach • Reuse refinement type architecture • Find a decidable refinement logic for • Tag-tests • Dictionaries • Lambdas • Define nested refinement type architecture ✓ ✓ ✗ ✓*
Nested Refinements 1 + d[f](0) • {|tag()=“Dict” • sel(,f):: • } d :: {|tag()=“Int”} {|tag()=“Int”} uninterpreted predicate“x::U” says“x has-type U” uninterpreted constantin the logic… … but syntactic arrowin the type system!
Nested Refinements • Refinement formulas over a decidable logic • uninterpreted functions, McCarthy arrays, linear arithmetic • Only base values refined by formulas All values T ::= {|p}U ::= x:T1T2p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | …| x :: U T ::= {|p} | x:T1T2p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | … traditional refinements
Nested Refinements • Refinement formulas over a decidable logic • uninterpreted functions, McCarthy arrays, linear arithmetic • Only base values refined by formulas • “has-type” allows “type terms” in formulas All values T ::= {|p}U ::= x:T1T2p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | … | x::U T ::= {|p} | x:T1T2p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | … traditional refinements
Nested Refinements • Refinement formulas over a decidable logic • uninterpreted functions, McCarthy arrays, linear arithmetic • Only base values refined by formulas • “has-type” allows “type terms” in formulas All values T ::= {|p}U ::= x:T1T2p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | … | x::U
let foofd = iftagoff=“Str”thend.n + d[f](0) elsed.n + f(0) f:{|Str()::IntInt} d:{|Dict() Int(.n) Str(f)[f]::IntInt} Int foo :: T ::= {|p}U ::= x:T1T2p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | … | x::U
let foofd = iftagoff=“Str”thend.n + d[f](0) elsed.n + f(0) f:{|Str()::IntInt} d:{|Dict() Int(.n) Str(f)[f]::IntInt} Int foo :: T ::= {|p}U ::= x:T1T2p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | … | x::U
let foofd = iftagoff=“Str”thend.n + d[f](0) elsed.n + f(0) {|tag()=“Int”} {|tag()=“Int”} f:{|Str()::IntInt} d:{|Dict() Int(.n) Str(f)[f]::IntInt} Int foo :: tag()=“Str” T ::= {|p}U ::= x:T1T2p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | … | x::U
let foofd = iftagoff=“Str”thend.n + d[f](0) elsed.n + f(0) f:{|Str()::IntInt} d:{|Dict() Int(.n) Str(f)[f]::IntInt} Int foo :: T ::= {|p}U ::= x:T1T2p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | … | x::U
let foofd = iftagoff=“Str”thend.n + d[f](0) elsed.n + f(0) sel(n,“n”) sel(n,f) f:{|Str()::IntInt} d:{|Dict() Int(.n) Str(f)[f]::IntInt} Int foo :: T ::= {|p}U ::= x:T1T2p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | … | x::U
let foofd = iftagoff=“Str”thend.n + d[f](0) elsed.n + f(0) f:{|Str()::IntInt} d:{|Dict() Int(.n) Str(f)[f]::IntInt} Int foo :: T ::= {|p}U ::= x:T1T2p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | … | x::U
let foofd = iftagoff=“Str”thend.n + d[f](0) elsed.n + f(0) foo :: f: {|tag()=“Str”::} IntInt d: {|tag()=“Dict” tag(sel(,“n”))=“Int” tag(f)=“Str” sel(,f):: } {|:: {|:: IntInt T ::= {|p}U ::= x:T1T2p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | … | x::U Int } }
Nested Refinements • Type Language • Subtyping • Extensions • Recap
Subtyping T ::= {|p} | x:T1T2 Subtyping Top Int {|true} {|tag()=“Int”} traditional refinements tag()=“Int”true Implication Int <: Top SMT Solver
Subtyping T ::= {|p} | x:T1T2 Subtyping Top Int {|true} {|tag()=“Int”} traditional refinements tag()=“Int” true tag()=“Int” tag()=“Int” Implication SMT Solver Int <: Top Int <: Int Top Int <: Int Int
Subtyping T ::= {|p} | x:T1T2 Subtyping Arrow Rule Top Int {|true} {|tag()=“Int”} traditional refinements tag()=“Int” true tag()=“Int” tag()=“Int” Implication SMT Solver Int <: Top Int <: Int Top Int <: Int Int
Subtyping T ::= {|p} | x:T1T2 Subtyping Arrow Rule traditional refinements • Decidable if: • Only values in formulas • Underlying theories decidable Implication SMT Solver • With nested refinements: • No new theories • But implication is imprecise!
Subtyping with Nesting Subtyping Implication ✗ ::TopInt::IntInt SMT Solver Invalid, as these are distinct uninterpreted constants
Subtyping with Nesting When goal is base predicate:pq When goal is “has-type” predicate:px::U Subtyping Subtyping Arrow Rule Implication Implication U’<:U px::U’ pq SMT Solver SMT Solver
Subtyping with Nesting UninterpretedReasoning Normalize formulas to subdivide obligations appropriately SyntacticReasoning + p ::TopInt TopInt<:IntInt p::Int Int
Nested Refinements • Type Language • Subtyping • Extensions • Recap
Extensions • Simple to add additional type constructors • Extend the grammar of type terms • Add additional syntactic subtyping rules U ::= x:T1T2 | A | List[T] | Null SyntacticRules Arrow Rule Covariant List Rule Null List Rule
Map ∀A,B. {|::AB}{|::List[A]}{|::List[B]} let mapfxs = ifxs=nullthen null else f xs[“hd”] :: map f xs[“tl”] encode recursive data as dictionaries
Filter let filterfxs = ifxs=nullthen null else if f xs[“hd”]then xs[“hd”] :: filter f xs[“tl”] else filter f xs[“tl”] • ∀A,B.{|::x:A{|n=Truex::B}} • {|::List[A]} • {|::List[B]} usual definition, but an interesting type
Dispatch let dispatch d f=d[f] d • ∀A,B.d:{|Dict() ::A} • {|Str() d[]::AB} • {|::B} a form of “bounded quantification” since d::A but additional constraints on A
Recap • Refinement types are a compelling approach • Dynamic dictionaries require dependency • Tag-tests require path sensitivity • But, not enough for lambdas in dictionaries • Nested refinement types are a clean solution • Natural way to describe dynamic idioms • Novel subtyping remains decidable and automatic • Interesting soundness proof
Research Plan System D lambdas, dictionaries, tag-tests + explicit references + prototype-based inheritance Dependent JavaScript translates to D++ Applications in DJS DJS {} typeofx := 1super + prototypes + references D
Research Plan System D lambdas, dictionaries, tag-tests + explicit references + prototype-based inheritance Dependent JavaScript translates to D++ Applications in DJS DJS {} typeofx := 1super + prototypes + references D
System D functionalupdate let x = {} in let x’ = x with “f” = 1 in x’.f JavaScript var x = {} x.f= 1 x.f x’ :: x :: {|=upd(x,“f”,1)} {|=empty} mutation!
System !D = D + Explicit References Update to reference cell doesn’t affect type So this dictionary read does not type check! JavaScript System !D var x = {} x.f= 1 x.f let x = ref {} in let _ = x := (!x with “f” = 1)in let _ = (!x)[“f”] x stores value of type {|Dict()} need to strongly update reference type
System !D = D + Explicit References • In JS, every dictionary is stored in a reference • No strong update cannot extend dictionary! • So, we must support flow-sensitive invariants var x = {} x.f= 1 x.f let x = ref {} in let _ = x := (!x with “f” = 1)in let _ = (!x)[“f”]
Strong Update à la Alias Types • Types are flow-insensitive (as usual) • But heap types are flow-sensitive • Mappings from locations to types can change U ::= … | RefL let x = ref {} in let _ = x := (!x with “f” = 1)in let _ = (!x)[“f”] x :: {|::RefL} safe given the updated heap type L::d: {|=empty} L::d’: {|=upd(d,“f”,1)}
One Last Feature: Prototypes • Each object is backed by a prototype object • Can be any ordinary object • Immutable link set at construction time* • Object read x[k] • If x has key k, return the binding • Else, recurse on x.__proto__[k] • Object write x[k]=y • does not affect prototype chain
var grandpa = { “surname” = “Quackenboss”, “saying” = “When I was your age…”, “age” = 77 } var parent = { “age” = 44 } var child = { “age” = 22 } var a = child.age Assume proto chain is: 1. does childhave age? 2. what is type of child.age? grandpa a :: a :: a :: {| = 22 = 44 = 77} {| = 22} {|Int()} parent child
System !D += Prototypes L3:: dGrandpa: L2:: dParent: L1:: dChild: Unknown Part of Heap • Track prototype links in heap type • immutable and acyclic • New uninterpreted predicate ObjHas(H,L,k) • analog to has(d,k) (macro forsel(d,k) != bot ) • New uninterpreted function ObjSel(H,L,k) • analog to sel(d,k) H T3 T2 T1 grandpa :: parent :: child :: {|::RefL3} {|::RefL2} {|::RefL1}