In this talk I present the design of Statix, a new constraint-based language for the executable specification of type systems. Statix specifications consist of predicates that define the well-formedness of language constructs in terms of built-in and user-defined constraints. Statix has a declarative semantics that defines whether a model satisfies a constraint. The operational semantics of Statix is defined as a sound constraint solving algorithm that searches for a solution for a constraint. The aim of the design is that Statix users can ignore the execution order of constraint solving and think in terms of the declarative semantics.
A distinctive feature of Statix is its use of scope graphs, a language parametric framework for the representation and querying of the name binding facts in programs. Since types depend on name resolution and name resolution may depend on types, it is typically not possible to construct the entire scope graph of a program before type constraint resolution. In (algorithmic) type system specifications this leads to explicit staging of the construction and querying of the type environment (class table, symbol table). Statix automatically stages the construction of the scope graph of a program such that queries are never executed when their answers may be affected by future scope graph extension. In the talk, I will explain the design of Statix by means of examples.
https://eelcovisser.org/post/309/declarative-type-system-specification-with-statix
Professional Resume Template for Software Developers
Statix: Declarative Type System Specification
1. Eelco Visser
WG2.16 | Portland | February 2019
Statix: Declarative Type System Specification
Based on:
Van Antwerpen, Bach Poulsen, Rouvoet, Visser
Scopes as Types
PACMPL 2 (OOPSLA), 2018
2. Context
- Language workbench
- High-level language specification
- Abstract from implementation concerns (generate automatically)
Type systems
- Type constraints
- Name resolution => scope graphs
- Staging
!2
Type System Specification
3. Scope Graphs
!3
0A : MOD(1) B : MOD(2)
1 2
P P
I
q : BOOLp : BOOL
::
: :
module A {
import B
def p : bool = ~q
}
module B {
def q : bool = true
}
B
P*
$ < P
7. Arithmetic Expressions: Type Predicate
!7
> 1 + 2 * 3
sorts TYPE constructors
INT : TYPE
BOOL : TYPE
FUN : TYPE * TYPE -> TYPE
rules // expressions
typeOfExp : scope * Exp -> TYPE
typeOfExp(s, Int(i)) = INT().
typeOfExp(s, Add(e1, e2)) = INT() :-
typeOfExp(s, e1) == INT(),
typeOfExp(s, e2) == INT().
. . .
typeOfExp(s, Eq(e1, e2)) = BOOL() :- {T}
typeOfExp(s, e1) == T,
typeOfExp(s, e2) == T.
constructors // arithmetic
Int : INT -> Exp
Add : Exp * Exp -> Exp
Sub : Exp * Exp -> Exp
Mul : Exp * Exp -> Exp
Eq : Exp * Exp -> Exp
8. Variable Definitions
!8
sorts Program constructors
Program : list(Decl) -> Program
sorts Decl constructors
Def : Bind -> Decl
Exp : Exp -> Decl
sorts Bind constructors
Bind : ID * Exp -> Bind
TBind : ID * Type * Exp -> Bind
relations
typeOfDecl : occurrence -> TYPE
def a = 0
def b = a + c
def b = 1 + d
def c : Int = 0
def e : Bool = 1
> a + b + c
9. Variable Definitions
!9
sorts Program constructors
Program : list(Decl) -> Program
sorts Decl constructors
Def : Bind -> Decl
Exp : Exp -> Decl
sorts Bind constructors
Bind : ID * Exp -> Bind
TBind : ID * Type * Exp -> Bind
relations
typeOfDecl : occurrence -> TYPE
rules
programOK : Program
programOK(Program(decls)) :- {s}
new s,
declsOk(s, decls).
rules // declarations
declOk : scope * Decl
declsOk maps declOk(*, list(*))
declOk(s, Def(bind)) :-
bindOk(s, s, bind).
declOk(s, Exp(e)) :- {T}
typeOfExp(s, e) == T.
def a = 0
def b = a + c
def b = 1 + d
def c : Int = 0
def e : Bool = 1
> a + b + c
10. Variable Definitions: Declarations
!10
sorts Program constructors
Program : list(Decl) -> Program
sorts Decl constructors
Def : Bind -> Decl
Exp : Exp -> Decl
sorts Bind constructors
Bind : ID * Exp -> Bind
TBind : ID * Type * Exp -> Bind
relations
typeOfDecl : occurrence -> TYPE
def a = 0
def b = a + c
def b = 1 + d
def c : Int = 0
def e : Bool = 1
> a + b + c
rules // bindings
bindOk : scope * scope * Bind
bindsOk maps bindOk(*, *, list(*))
bindOk(s_bnd, s_ctx, Bind(x, e)) :- {T}
typeOfExp(s_ctx, e) == T,
s_bnd -> Var{x@x} with typeOfDecl T.
bindOk(s_bnd, s_ctx, TBind(x, t, e)) :- {T}
typeOfType(s_ctx, t) == T,
typeOfExp(s_ctx, e) == T,
s_bnd -> Var{x@x} with typeOfDecl T.
11. Variable Definitions: Name Resolution
!11
rules // bindings
bindOk : scope * scope * Bind
bindsOk maps bindOk(*, *, list(*))
bindOk(s_bnd, s_ctx, Bind(x, e)) :- {T}
typeOfExp(s_ctx, e) == T,
s_bnd -> Var{x@x} with typeOfDecl T.
bindOk(s_bnd, s_ctx, TBind(x, t, e)) :- {T}
typeOfType(s_ctx, t) == T,
typeOfExp(s_ctx, e) == T,
s_bnd -> Var{x@x} with typeOfDecl T.
rules // variables
typeOfExp(s, Var(x)) = T :- {p d }
typeOfDecl of Var{x@x} in s |-> [(p, (d, T))].
sorts Program constructors
Program : list(Decl) -> Program
sorts Decl constructors
Def : Bind -> Decl
Exp : Exp -> Decl
sorts Bind constructors
Bind : ID * Exp -> Bind
TBind : ID * Type * Exp -> Bind
relations
typeOfDecl : occurrence -> TYPE
def a = 0
def b = a + c
def b = 1 + d
def c : Int = 0
def e : Bool = 1
> a + b + c
12. Lexical Scope: Functions
!12
rules // functions
typeOfExp(s, Fun(x, t, e)) = FUN(T, S) :- {s_fun}
typeOfType(s, t) == T,
new s_fun,
s_fun -P-> s,
s_fun -> Var{x@x} with typeOfDecl T,
typeOfExp(s_fun, e) == S.
typeOfExp(s, App(e1, e2)) = T :- {S}
typeOfExp(s, e1) == FUN(S, T),
typeOfExp(s, e2) == S.
sorts TYPE constructors
INT : TYPE
BOOL : TYPE
FUN : TYPE * TYPE -> TYPE
def i = 3
def inc = fun(x : Int) { x + i }
> inc 2
constructors // functions
Fun : ID * Type * Exp -> Exp
App : Exp * Exp -> Exp
13. Lexical Scope: Sequential Let
!13
rules // let bindings
typeOfExp(s, Let(binds, e)) = T :- {s_let}
new s_let,
sbindsOk(s, s_let, binds),
typeOfExp(s_let, e) == T.
def a = 0
def b = 1
def c = 2
> let
a = c;
b = a;
c = b
in
a + b + c
rules // bindings
sbindsOk : scope * scope * list(Bind)
sbindsOk(s, s_fin, []) :-
s_fin -P-> s.
sbindsOk(s, s_fin, [bind | binds]) :- {s_mid}
new s_mid, s_mid -P-> s,
bindOk(s_mid, s, bind),
sbindsOk(s_mid, s_fin, binds).
14. Lexical Scope: Parallel Let
!14
rules // let bindings
typeOfExp(s, LetPar(binds, e)) = T :- {s_let}
new s_let, s_let -P-> s,
bindsOk(s_let, s, binds),
typeOfExp(s_let, e) == T.
bindOk : scope * scope * Bind
bindsOk maps bindOk(*, *, list(*))
bindOk(s_bnd, s_ctx, Bind(x, e)) :- {T}
typeOfExp(s_ctx, e) == T,
s_bnd -> Var{x@x} with typeOfDecl T.
def a = 0
def b = 1
def c = 2
> letpar
a = c;
b = a;
c = b
in
a + b + c
15. Lexical Scope: Recursive Let
!15
rules // let bindings
typeOfExp(s, LetRec(binds, e)) = T :- {s_let}
new s_let, s_let -P-> s,
bindsOk(s_let, s_let, binds),
typeOfExp(s_let, e) == T.
bindOk : scope * scope * Bind
bindsOk maps bindOk(*, *, list(*))
bindOk(s_bnd, s_ctx, Bind(x, e)) :- {T}
typeOfExp(s_ctx, e) == T,
s_bnd -> Var{x@x} with typeOfDecl T.
> letrec
odd = fun(x : Int) {
if x == 0 then false
else even(x - 1)
};
even = fun(x : Int) {
if x == 0 then true
else odd(x - 1)
}
in
even(3)
17. Modules: Scopes as Types
!17
rules // modules
declOk(s, Module(m, decls)) :- {s_mod}
new s_mod, s_mod -P-> s,
s -> Mod{m@m} with typeOfDecl MOD(s_mod),
declsOk(s_mod, decls).
declOk(s, Import(m)) :- {p d s_mod}
typeOfDecl of Mod{m@m} in s |-> [(p, (d, MOD(s_mod)))],
s -I-> s_mod.
module A {
import B
def a = 4
def c = b + 4
}
module B {
import A
def b = a + 3
}
sorts TYPE constructors
INT : TYPE
BOOL : TYPE
FUN : TYPE * TYPE -> TYPE
MOD : scope -> TYPE
name-resolution
labels P I R
resolve Var filter pathMatch[P* (R* | I*)]
min pathLt[$ < I, $ < P, I < P, R < P]
resolve Mod filter pathMatch[P P* I*]
min pathLt[$ < I, $ < P, I < P, R < P]
18. Records
!18
record Point {
x : Int
y : Int
}
def p : Point
= new Point { x = 1, y = 2}
> p.x + p.y
def z = 3
> with p do x + y + z
19. Record Type Declaration: Scopes as Types
!19
rules // record type
declOk(s, Record(x, fdecls)) :- {s_rec}
new s_rec,
fdeclsOk(s_rec, s, fdecls),
s -> Var{x@x} with typeOfDecl REC(s_rec).
fdeclOk : scope * scope * FDecl
fdeclsOk maps fdeclOk(*, *, list(*))
fdeclOk(s_bnd, s_ctx, FDecl(x, t)) :- {T}
typeOfType(s_ctx, t) == T,
s_bnd -> Var{x@x} with typeOfDecl T.
sorts TYPE constructors
INT : TYPE
BOOL : TYPE
FUN : TYPE * TYPE -> TYPE
REC : scope -> TYPE
record Point {
x : Int
y : Int
}
def p : Point
= new Point { x = 1, y = 2}
> p.x + p.y
def z = 3
> with p do x + y + z
20. Record Literals
!20
rules // records construction
typeOfExp(s, New(x, fbinds)) = REC(s_rec) :- {p d}
typeOfDecl of Var{x@x} in s |-> [(p, (d, REC(s_rec)))],
fbindsOk(s, s_rec, fbinds).
fbindOk : scope * scope * FBind
fbindsOk maps fbindOk(*, *, list(*))
fbindOk(s, s_rec, FBind(x, e)) :- {p d T}
typeOfExp(s, e) == T,
typeOfDecl of Var{x@x} in s_rec |-> [(p, (d, T))].
record Point {
x : Int
y : Int
}
def p : Point
= new Point { x = 1, y = 2}
> p.x + p.y
def z = 3
> with p do x + y + z
sorts TYPE constructors
INT : TYPE
BOOL : TYPE
FUN : TYPE * TYPE -> TYPE
REC : scope -> TYPE
21. Record Projection
!21
rules // record projection
typeOfExp(s, Proj(e, x)) = T :- {p d s_rec S}
typeOfExp(s, e) == S,
proj(S, x) == T.
proj : TYPE * ID -> TYPE
proj(REC(s_rec), x) = T :- {p d}
typeOfDecl of Var{x@x} in s_rec |-> [(p, (d, T))].
record Point {
x : Int
y : Int
}
def p : Point
= new Point { x = 1, y = 2}
> p.x + p.y
def z = 3
> with p do x + y + z
sorts TYPE constructors
INT : TYPE
BOOL : TYPE
FUN : TYPE * TYPE -> TYPE
REC : scope -> TYPE
22. With Record
!22
rules // with record value
typeOfExp(s, With(e1, e2)) = T :- {s_with s_rec}
typeOfExp(s, e1) == REC(s_rec),
new s_with,
s_with -P-> s, s_with -R-> s_rec,
typeOfExp(s_with, e2) == T.
record Point {
x : Int
y : Int
}
def p : Point
= new Point { x = 1, y = 2}
> p.x + p.y
def z = 3
> with p do x + y + z
sorts TYPE constructors
INT : TYPE
BOOL : TYPE
FUN : TYPE * TYPE -> TYPE
REC : scope -> TYPE
name-resolution
labels P I R
resolve Var filter pathMatch[P* (R* | I*)]
min pathLt[$ < I, $ < P, I < P, R < P]
23. Type References
!23
record Point {
x : Int
y : Int
}
def translate : Point -> Point -> Point
= fun(p: Point){ fun(d: Point) {
new Point{
x = p.x + d.x,
y = p.y + d.y }
} }
def p : Point = new Point { x = 1, y = 2}
> translate(p)(p)
rules // types
typeOfType : scope * Type -> TYPE
typeOfType(s, IntT()) = INT().
typeOfType(s, BoolT()) = BOOL().
typeOfType(s, FunT(t1, t2)) =
FUN(typeOfType(s, t1), typeOfType(s, t2)).
typeOfType(s, RecT(x)) = REC(s_rec) :- {p d}
typeOfDecl of Var{x@x}
in s |-> [(p, (d, REC(s_rec)))].
25. Implementation
!25
type point = {x : num, y : num} in
let mkpoint = fun(x : num) { {x = x, y = x} } in
type color = num in
type colorpoint =
{k : color} extends point in
let addColor =
fun(c : num) {
fun(p : colorpoint) {
({c = c} extends p) : colorpoint
}
} in
(addColor 6 ({c = 5} extends mkpoint 4)) : colorpoint
typeOfExp : scope * Exp -> Type
typeOfExp(s, Num(_)) = NUM().
typeOfExp(s, Plus(e1, e2)) = NUM() :-
typeOfExp(s, e1) == NUM(),
typeOfExp(s, e2) == NUM().
typeOfExp(s, Fun(x, te, e)) = FUN(S, T) :- {s_fun}
typeOfTypeExp(s, te) == S,
new s_fun, s_fun -P-> s,
s_fun -> Var{x@x} with typeOfDecl S,
typeOfExp(s_fun, e) == T.
typeOfExp(s, Var(x)) = T :-
query typeOfDecl filter pathMatch[P*(R|E)*] and { d :- varOrFld(x, d) }
min pathLt[$ < P, $ < R, $ < E, R < P, R < E] and true
in s |-> [(_, (_, T))].
typeOfExp(s, App(e1, e2)) = T :- {S U}
typeOfExp(s, e1) == FUN(S, T),
typeOfExp(s, e2) == U,
subType(U, S).
type point = {x : num, y : num} in
let mkpoint = fun(x : num) { {x = x, y = x} } in
type color = num in
type colorpoint =
{k : color} extends point in
let addColor =
fun(c : num) {
fun(p : colorpoint) {
({c = c} extends p) : colorpoint
}
} in
(addColor 6 ({c = 5} extends mkpoint 4)) : colorpoint
Program
Statix Specification
Typed Program
Solver
package mb.statix.solver;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.immutables.value.Value;
import org.metaborg.util.functions.Predicate1;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import mb.nabl2.terms.ITerm;
import mb.nabl2.terms.ITermVar;
import mb.nabl2.terms.unification.IUnifier;
import mb.nabl2.util.TermFormatter;
import mb.statix.solver.log.IDebugContext;
import mb.statix.solver.log.LazyDebugContext;
import mb.statix.solver.log.Log;
public class Solver {
private Solver() {
}
public static SolverResult solve(final State state, final Iterable<IConstraint> constraints,
final Completeness completeness, final IDebugContext debug) throws InterruptedException {
return solve(state, constraints, completeness, v -> false, s -> false, debug);
}
public static SolverResult solve(final State _state, final Iterable<IConstraint> _constraints,
final Completeness _completeness, Predicate1<ITermVar> isRigid, Predicate1<ITerm> isClosed,
final IDebugContext debug) throws InterruptedException {
debug.info("Solving constraints");
final LazyDebugContext proxyDebug = new LazyDebugContext(debug);
// set-up
final Set<IConstraint> constraints = Sets.newConcurrentHashSet(_constraints);
State state = _state;
Completeness completeness = _completeness;
completeness = completeness.addAll(constraints);
// fixed point
final Set<IConstraint> failed = Sets.newHashSet();
final Log delayedLog = new Log();
final Map<IConstraint, Delay> delays = Maps.newHashMap();
boolean progress = true;
int reduced = 0;
int delayed = 0;
outer: while(progress) {
progress = false;
delayedLog.clear();
delays.clear();
final Iterator<IConstraint> it = constraints.iterator();
while(it.hasNext()) {
if(Thread.interrupted()) {
throw new InterruptedException();
}
final IConstraint constraint = it.next();
proxyDebug.info("Solving {}", constraint.toString(Solver.shallowTermFormatter(state.unifier())));
IDebugContext subDebug = proxyDebug.subContext();
try {
Optional<ConstraintResult> maybeResult =
constraint.solve(state, new ConstraintContext(completeness, isRigid, isClosed, subDebug));
progress = true;
it.remove();
completeness = completeness.remove(constraint);
reduced += 1;
if(maybeResult.isPresent()) {
final ConstraintResult result = maybeResult.get();
state = result.state();
if(!result.constraints().isEmpty()) {
final List<IConstraint> newConstaints = result.constraints().stream()
.map(c -> c.withCause(constraint)).collect(Collectors.toList());
subDebug.info("Simplified to {}", toString(newConstaints, state.unifier()));
constraints.addAll(newConstaints);
completeness = completeness.addAll(newConstaints);
}
} else {
subDebug.error("Failed");
failed.add(constraint);
if(proxyDebug.isRoot()) {
printTrace(constraint, state.unifier(), subDebug);
} else {
proxyDebug.info("Break early because of errors.");
break outer;
}
}
proxyDebug.commit();
} catch(Delay d) {
subDebug.info("Delayed");
delayedLog.absorb(proxyDebug.clear());
delays.put(constraint, d);
delayed += 1;
}
}
}
delayedLog.flush(debug);
debug.info("Solved {} constraints ({} delays) with {} failed and {} remaining constraint(s).", reduced, delayed,
failed.size(), constraints.size());
return SolverResult.of(state, completeness, failed, delays);
}
public static Optional<SolverResult> entails(final State state, final Iterable<IConstraint> constraints,
final Completeness completeness, final IDebugContext debug) throws InterruptedException, Delay {
return entails(state, constraints, completeness, ImmutableSet.of(), debug);
}
public static Optional<SolverResult> entails(final State state, final Iterable<IConstraint> constraints,
final Completeness completeness, final Iterable<ITermVar> _localVars, final IDebugContext debug)
throws InterruptedException, Delay {
debug.info("Checking entailment of {}", toString(constraints, state.unifier()));
final Set<ITermVar> localVars = ImmutableSet.copyOf(_localVars);
final Set<ITermVar> rigidVars = Sets.difference(state.vars(), localVars);
final SolverResult result = Solver.solve(state, constraints, completeness, rigidVars::contains,
state.scopes()::contains, debug.subContext());
if(result.hasErrors()) {
debug.info("Constraints not entailed");
return Optional.empty();
} else if(result.delays().isEmpty()) {
debug.info("Constraints entailed");
return Optional.of(result);
} else {
debug.info("Cannot decide constraint entailment (unsolved constraints)");
throw result.delay(); // FIXME Remove local vars and scopes
}
}
private static void printTrace(IConstraint failed, IUnifier unifier, IDebugContext debug) {
@Nullable IConstraint constraint = failed;
while(constraint != null) {
debug.error(" * {}", constraint.toString(Solver.shallowTermFormatter(unifier)));
constraint = constraint.cause().orElse(null);
}
}
private static String toString(Iterable<IConstraint> constraints, IUnifier unifier) {
final StringBuilder sb = new StringBuilder();
boolean first = true;
for(IConstraint constraint : constraints) {
if(first) {
first = false;
} else {
sb.append(", ");
}
sb.append(constraint.toString(Solver.shallowTermFormatter(unifier)));
}
return sb.toString();
}
@Value.Immutable
public static abstract class ASolverResult {
@Value.Parameter public abstract State state();
@Value.Parameter public abstract Completeness completeness();
@Value.Parameter public abstract Set<IConstraint> errors();
public boolean hasErrors() {
return !errors().isEmpty();
}
@Value.Parameter public abstract Map<IConstraint, Delay> delays();
public Delay delay() {
ImmutableSet.Builder<ITermVar> vars = ImmutableSet.builder();
ImmutableMultimap.Builder<ITerm, ITerm> scopes = ImmutableMultimap.builder();
delays().values().stream().forEach(d -> {
vars.addAll(d.vars());
scopes.putAll(d.scopes());
});
return new Delay(vars.build(), scopes.build());
}
}
public static TermFormatter shallowTermFormatter(final IUnifier unifier) {
return t -> unifier.toString(t, 3);
}
}
Scope Graph
26. In the paper
- Typing rules for STLC+records, System F, Featherweight Java
- Resolution calculus of scope graphs
- Declarative semantics of Statix
- Description of solver algorithm of Statix
In the artifact
- Implementation of scope graphs and Statix
- Executable specs of STLC+records, System F, Featherweight
Generic Java
!26
Other Contributions