Nested Refinements:A Logic for Duck
Typing
Ravi Chugh, Pat Rondon, Ranjit Jhala (UCSD)
::
2
What are “Dynamic Languages”?
let onto callbacks f obj = if f = null then new List(obj, callbacks) else let cb = if tagof f = “Str” then obj[f] else f in new List(fun () -> cb obj, callbacks)
tag tests affect control flow
dictionary objects indexed by arbitrary string keys
first-class functions can appear inside objects
3
Problem: Lack of static types… makes rapid prototyping / multi-language applications easy
… makes reliability / performance / maintenance hard
This Talk: System D… a type system for these features
tag tests affect control flow
dictionary objects indexed by arbitrary string keys
first-class functions can appear inside objects
…
occurrence types
∨, ∧
Our Approach: Quantifier-free formulas
tag tests affect control flow
dictionary objects indexed by arbitrary string keys
first-class functions can appear inside objects
Expressiveness
Usability
F≤
Coq
refinement types
nested refinements
syntactic approaches dependent approaches
1. increase expressiveness
2. retain level of automation
5
Functions inside dictionariesChallenge:
{|tag() = “Int”tag() = “Bool”}x ::
{|tag() = “Dict” tag(sel(,“n”)) = “Int”tag(sel(,m)) = “Int”}
d ::
tag tests affect control flow
dictionary objects indexed by arbitrary string keys
first-class functions can appear inside objects
6
Key Idea: Nested Refinements
{|tag() = “Dict”
sel(,f) ::
}
d ::
{|tag()=“Int” }
{|tag()=“Int” }
uninterpreted predicate“x :: U” says“x has-type U”
syntactic arrow type…
1 + d[f](0)
7
Key Idea: Nested Refinements
{|tag() = “Dict”
sel(,f) ::
}
d ::
{|tag()=“Int” }
{|tag()=“Int” }
uninterpreted predicate“x :: U” says“x has-type U”
1 + d[f](0)
syntactic arrow type…
… but uninterpreted constant in the logic
8
• All values described by refinement formulasT ::= {|p}
• “Has-type” predicate for arrows in formulasp ::= … | x :: y:T1T2
• Can express idioms of dynamic languages
• Automatic type checking– Decidable refinement logic
– Subtyping = SMT Validity + Syntactic Subtyping
Key Idea: Nested Refinements
Outline
Intro
Examples
Subtyping
Type Soundness
Conclusion
9
10
let negate x = if tagof x = “Int” then 0 – x else not x
y:Top{| = tag(y)}tagof ::
x:{|tag() = “Int” tag() = “Bool”}
{|tag() = tag(x)}
x:IntOrBool{|tag() = tag(x)}
y:{|true}
11
IntOrBoolx ::Γ
{|Int()}x ::
{|Int()}0 - x ::
x:IntOrBool{|tag() = tag(x)}
let negate x = if tagof x = “Int” then 0 – x else not x
tag(x) = “Int”
type environment
✓
✓SMT Solver
✓
12
IntOrBoolx ::Γ
{|Bool()}x ::
{|Bool()}not x ::
x:IntOrBool{|tag() = tag(x)}
let negate x = if tagof x = “Int” then 0 – x else not x
not (tag(x) = “Int”)
type environment
SMT Solver
✓✓
✓
13
{| :: }x: IntOrBool {|tag() = tag(x) }
x:IntOrBool{|tag() = tag(x)}
Nesting structure hidden with syntactic sugar
14
Dictionary Operations
d:Dictk:Str {| = true has(d,k)}mem ::
d:Dictk:{|has(d,)}{| = sel(d,k)}get ::
d:Dictk:Strx:Top {| = upd(d,k,x)}set ::
Types in terms of McCarthy operators
15
let getCount d c = if mem d c then toInt (d[c]) else 0
d:Dictc:StrInt
{| = true has(d,c)}
safe dictionary key lookup
get d c
16
let incCount d c = let i = getCount d c in {d with c = i + 1}
d:Dictc:StrInt
d:Dictc:Str{|EqMod(,d,c) Int([c])}
tag(sel(,c)) = “Int”
let getCount d c = if mem d c then toInt (d[c]) else 0
set d c (i+1)
17
T ::= {|p}
p ::= … | x :: U
U ::= y:T1T2
| A
| List T
| Null
Adding Type Constructors
“type terms”
18
let apply f x = f x
∀A,B. {|:: {|:: A }{|:: B } }
{|:: A }
{|:: B }
∀A,B. (AB) A B
19
let dispatch d f = d[f](d)
∀A,B. d:{| :: A } f:{|d[] :: AB } {| :: B }
d :: A but additional constraints on A
≈ ∀A <: {f: A B}. d :: A
a form of“bounded quantification”
20
let map f xs = if xs = null then null else new List(f xs[“hd”], map f xs[“tl”])
∀A,B.
{|:: AB } {|:: List[A] } {|:: List[B] }
encode recursive data as dictionaries
∀A,B. (AB) List[A] List[B]
21
let filter f xs = if xs = null then null else if not (f xs[“hd”]) then filter f xs[“tl”] else new List(xs[“hd”], filter f xs[“tl”])
∀A,B. (x:A{|n = true x:: B } List[A]List[B]
usual definition,but an interesting type
Outline
Intro
Examples
Subtyping
Type Soundness
Conclusion
22
23
SMT Γ =42 tag() = “Int”
Γ {|= 42} < Int_|
(Int, IntInt)IntapplyInt ::
applyInt (42, negate)
x:IntOrBool{|tag() = tag(x)}negate ::
Γ
✓
type environment
24
SMT
… negate:: x:IorB{|tag() = tag(x)}
… =negate
:: IntInt
Γ {|= negate } < {|:: IntInt }_|
(Int, IntInt)IntapplyInt ::
applyInt (42, negate)
x:IntOrBool{|tag() = tag(x)}negate ::
Γ
type environment
25
Γ {|= negate } < {|:: IntInt }_|
(Int, IntInt)IntapplyInt ::
applyInt (42, negate)
x:IntOrBool{|tag() = tag(x)}negate ::
Γ
✗ distinct uninterpreted
constants!
type environment
SMT
… negate:: x:IorB{|tag() = tag(x)}
… =negate
:: IntInt
26
IorB {|tag() = tag(x)} <: Int Int
Int <: IorB {|tag() = tag(x)} <: Int
tag() = “Int” tag() = “Int” tag() = “Bool”
tag() = “Int” tag() = tag(x) tag() = “Int”
Invalid, since these are uninterpreted constants
Want conventional syntactic subtyping
✓ ✓
:: x:IorB{|tag() = tag(x)}
:: Int Int✗
27
Subtyping with Nesting
base predicate: p qij
1) Convert q to CNF clauses (q11 … ) … (qn1 … )
2) For each clause, discharge some literal qij as follows:
To prove p q :
anything except x :: Ue.g. tag() = tag(x)
e.g. tag(sel(d,k)) = “Int”
28
Subtyping with Nesting
Implication
SMT Solver
Subtyping Arrow Rule
base predicate: p qij “has-type” predicate: p x :: U
Implication
SMT Solver
Subtyping
p x :: U’
U’ <: U
p qij
1) Convert q to CNF clauses (q11 … ) … (qn1 … )
2) For each clause, discharge some literal qij as follows:
To prove p q :
29
UninterpretedReasoning
… negate:: x:IorB{|tag() = tag(x)}
… =negate
:: x:IorB{|tag() = tag(x)}
applyInt (42, negate)
Γ {|= negate } < {|:: IntInt }_|
+
SyntacticReasoning
Γ x:IorB{|tag() = tag(x)} <: IntInt_|
Outline
Intro
Examples
Subtyping
Type Soundness
Conclusion
30
31
0 :: {|x.x+1:: IntInt }_|
x.x+1 :: {|:: IntInt }_|
f:{|:: IntInt }0 :: {|f:: IntInt }_|
SubstitutionLemma
_| v :: TxandIf x:Tx, Γ e[:: T_|
Γ[v/x] e[v/x] :: T[v/x]_|then
independent of 0, and just echoes the binding from the environment
32
0 :: {|x.x+1:: IntInt }_|
SMT =0 x.x+1:: IntInt✗{|=0 } < {|x.x+1:: IntInt }0 :: {|
=0 }
SubstitutionLemma
_| v :: TxandIf x:Tx, Γ e[:: T_|
Γ[v/x] e[v/x] :: T[v/x]_|then
1st attempt
33
0 :: {|x.x+1:: IntInt }_|
{|=0 } < {|x.x+1:: IntInt }0 :: {|=0 }
SubstitutionLemma
_| v :: TxandIf x:Tx, Γ e[:: T_|
Γ[v/x] e[v/x] :: T[v/x]_|then✗SMT =0 :: U’
Arrow U’ <: IntInt
+2nd attempt✗
34
x.x+1:: IntInt|=I
x.x+1:: {|:: IntInt }|_
SMT Γ p q
Γ {|=p } < {|=q }_|
[S-Valid-Uninterpreted]
|=I Γ p q
Γ {|=p } < {|=q }_|
[S-Valid-Interpreted]
iff
• Rule not closed under substitution
• Interpret formulas by “hooking back” into type system
• Stratification to create ordering for induction
n
n-1
n
n
35
Type Soundness
StratifiedSubstitutionLemma
_| v :: TxandIf x:Tx, Γ e[:: T_|
Γ[v/x] e[v/x] :: T[v/x]_|then n+1
n
n
StratifiedPreservation
e vandIf e[:: T_| 0
_|then v[:: Tm for some m
“Level 0” for type checking source programs,using only [S-Valid-Uninterpreted]
artifact of the metatheory
36
Recap• Dynamic languages make heavy use of:
– run-time tag tests, dictionary objects, lambdas
• Nested refinements– generalizes refinement type architecture– enables combination of dictionaries and lambdas
• Decidable refinement logic– all proof obligations discharged algorithmically– novel subtyping decomposition to retain precision
• Syntactic type soundness
37
Future Work• Imperative Updates
• Inheritance (prototypes in JS, classes in Python)
• Applications
• More local type inference / syntactic sugar
• Dictionaries in statically-typed languages
38
Thanks!
Dravichugh.com/nested
::
39
Extra Slides
40
Constants
tagof :: x:Top{| = tag(x)}
mem :: d:Dictk:Str {|Bool() = True has(d,k)}
get :: d:Dictk:{|Str() has(d,)}{| = sel(d,k)}
set :: d:Dictk:Strx:Top {| = upd(d,k,x)}
rem :: d:Dictk:Str{| = upd(d,k,bot)}
41
• Types
• Formulas
• Logical Values
Macros{|tag()=“Int” }
Int
x:T1T2 {|:: }x:T1T2
tag(x) = “Str”Str(x)
sel(n,“k”)x.k
sel(n,k)x[k]
sel(d,k) != bothas(d,k)
∀k’. k’ != k sel(d,k) != sel(d’,k)
EqMod(d,d’,k)
42
Ontolet onto callbacks f obj = if f = null then new List(obj,callbacks) else let cb = if tagof f = “Str” then obj[f] else f in new List(fun () -> cb obj, callbacks)
∀A. callbacks:List[TopTop]
f:{|=null Str():: ATop }
obj:{|:: A
(f=null :: AInt)
(Str(f) [f] :: AInt) }
List[TopTop]
onto ::
functional version of Dojo function
43
Onto (2)let onto (callbacks,f,obj) = if f = null then new List(obj,callbacks) else let cb = if tagof f = “Str” then obj[f] else f in new List(fun () -> cb obj, callbacks)
callbacks:List[TopTop]
* f:{g|g=null Str(g)g:: {x|x= obj}Top }
* obj:{o|(f=null o :: {x|x= o}Int)
(Str(f) o[f] :: {x|x= o}Int) }
List[TopTop]
onto ::
functional version of Dojo function
44
Traditional vs. Nested Refinements
45
• Reuse refinement type architecture
• Find a decidable refinement logic for– Tag-tests– Dictionaries– Lambdas
• Define nested refinement type architecture
Approach: Refinement Types
✓
✗✓✓*
46
Nested Refinements
T ::= {|p}U ::= x:T1T2
p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | … | x :: U
• Refinement formulas over a decidable logic– uninterpreted functions, McCarthy arrays, linear arithmetic
• Only base values refined by formulasAll values
T ::= {|p} | x:T1T2
p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | …
traditional refinements
47
Nested Refinements
T ::= {|p}U ::= x:T1T2
p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | … | x::U
• 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} | x:T1T2
p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | …
traditional refinements
48
Nested Refinements
T ::= {|p}U ::= x:T1T2
p ::= pq | … | x = y | x < y | … | tag(x) = “Int” | … | sel(x,y) = z | … | x::U
• 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
49
Subtyping (Traditional Refinements)
T ::= {|p} | x:T1T2
traditional refinements
Implication
SMT Solver
Subtyping
tag()=“Int” true
Int <: Top
50
Subtyping (Traditional Refinements)
T ::= {|p} | x:T1T2
traditional refinements
Implication
SMT Solver
Subtyping
Top Int <: Int Int
Int <: Top Int <: Int
tag()=“Int” true
tag()=“Int” tag()=“Int”
51
Subtyping (Traditional Refinements)
T ::= {|p} | x:T1T2
traditional refinements
Implication
SMT Solver
Subtyping
Top Int <: Int Int
Int <: Top Int <: Int
tag()=“Int” true
tag()=“Int” tag()=“Int”
Arrow Rule
52
Subtyping (Traditional Refinements)
T ::= {|p} | x:T1T2
traditional refinements
Implication
SMT Solver
Subtyping Arrow Rule
Decidable if:• Only values in formulas• Underlying theories decidable