A right-to-left type system for mutually-recursive value definitions

11/20/2018 ∙ by Alban Reynaud, et al. ∙ 0

In call-by-value languages, some mutually-recursive value definitions can be safely evaluated to build recursive functions or cyclic data structures, but some definitions (let rec x = x + 1) contain vicious circles and their evaluation fails at runtime. We propose a new static analysis to check the absence of such runtime failures. We present a set of declarative inference rules, prove its soundness with respect to the reference source-level semantics of Nordlander, Carlsson, and Gill (2008), and show that it can be (right-to-left) directed into an algorithmic check in a surprisingly simple way. Our implementation of this new check replaced the existing check used by the OCaml programming language, a fragile syntactic/grammatical criterion which let several subtle bugs slip through as the language kept evolving. We document some issues that arise when advanced features of a real-world functional language (exceptions in first-class modules, GADTs, etc.) interact with safety checking for recursive definitions.

READ FULL TEXT VIEW PDF
POST COMMENT

Comments

There are no comments yet.

Authors

page 1

page 2

page 3

page 4

This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1 Introduction

OCaml is a statically-typed functional language of the ML family. One of the features of the language is the let rec operator, which is usually used to define recursive functions. For example, the following code defines the factorial function:

let rec fac x =
  if x = 0 then 1
  else x * (fac (x - 1))

Beside functions, —let rec— can also be used to define recursive values, as in the following definition of an infinite list —ones— where every element is —1—.

let rec ones = 1 :: ones

Note that this “infinite” list is actually cyclic: it uses a finite amount of memory, because it is composed of a single cons-cell referencing itself.

However, not all recursive definitions can be computed. For example, here is a definition that is justly rejected by the compiler:

let rec x = 1 + x

The variable x, which is typed as an integer, is used in its own definition. Computing 1 + x requires the variable x to have a known value: this definition contains a vicious circle, and any runtime evaluation strategy would fail if it is accepted by the language.

Functional languages have different ways to deal with recursive values. Standard ML takes a simple approach, rejecting all recursive definitions that are not recursive function values. At the other extreme, the lazy language Haskell accepts every well-typed recursive definition, although some of them lead to infinite computation. In OCaml, safe cyclic-values definitions are accepted, and they are occasionally useful.

For a cute example, consider an interpreter for a small programming language with datatypes for ASTs and for values:

type ast = Fun of var * expr | $\ldots$
type value = Closure of env * var * expr | $\ldots$

Our interpretation function —eval— takes an environment of type —(var * value) list— and an —ast— and builds a —value—

let rec eval env = function
  | 
  | Fun (x, t) -> Closure(env, x, t)

Now consider adding an —ast— constructor FunRec of var * var * expr for recursive functions: FunRec (”f”, x”, t) represents the recursive function , or . Our OCaml interpreter can use value recursion to build a closure for these recursive functions, without changing the type of the Closure constructor: the recursive closure simply adds itself to the closure environment.

let rec eval env = function
  | 
  | Fun (x, t) -> Closure(env, x, t)
  | FunRec (f, x, t) ->
    let rec clo = Closure((f,clo)::env, x, t) in clo

Until recently, the static check used by OCaml to reject vicious recursive definitions relied on a syntactic/grammatical description. While we believe that the check as originally defined was correct, it proved fragile and difficult to maintain as the language evolved and new features interacted with recursive definitions. Over the year, several bugs were found where the check was unduly lenient. In conjunction with OCaml’s efficient compilation scheme for recursive definitions (hirschowitz-compilation-2009), this leniency resulted in memory safety violations, and led to segmentation faults.

Seeking to address these problems, we have designed and implemented a new recursive check for safety of recursive definitions, based on a novel static analysis, formulated as a simple type system. Our implementation was integrated into the main OCaml distribution in August 2018.

In the present document, we formally describe our analysis, presented using a core ML language restricted to the salient features for value recursion (§3). We present inference rules (§LABEL:section:inference-rules), study the meta-theory of the analysis, and show that it is sound and complete? with respect to the operational semantics proposed by dynamic-semantics-2008LABEL:section:meta-theory). We also discuss the challenges caused by scaling the analysis to OCaml (§LABEL:section:extension-full), a full-fledged functional language, in particular the delicate interactions with non-uniform value representations (§LABEL:section:float-arrays), with exceptions and first-class modules (§LABEL:section:exceptions), and with Generalized Algebraic Datatypes (GADTs) (§LABEL:section:gadts).

1.0.1 Contributions

We studied related work in search of an inference system that could be used, as-is or with minor modifications, for our analysis – possibly neglecting finer-grained details of the system that we do not need. We did not find any. Existing systems, detailed in our related work section (§LABEL:subsec:related-work), have a finer-grained handling of functions (in particular ML functors), but coarser-grained handling of cyclic data, and most do not propose effective inference algorithms.

We claim the following contributions:

  • We propose a new system of inference rules that captures the needs of OCaml (or F) recursive value definitions, previously described by ad-hoc syntactic restrictions (§LABEL:section:inference-rules). We implemented a checker derived from these rules, scaled up to the full OCaml language and integrated in the OCaml implementation.

  • We prove the soundness of our analysis with respect to a pre-existing source-level operational semantics: accepted recursive values evaluate without vicious-circle failures (§LABEL:section:meta-theory).

  • Our analysis is less fine-grained on functions than existing works (thanks to a less demanding problem domain), but in exchange it is noticeably simpler.

  • The idea of right-to-left computational interpretation (from type to environment) helps bring the complexity down – a declarative presentation designed for a left-to-right reading would be more complex. It is novel in this design space and could inspire other inference rules designers.

2 Overview

2.1 Access modes

Our analysis is based on the classification of each use of a recursively-defined variable using “access modes” or “usage modes” . These modes represent the degree of access needed to the value bound to the variable during evaluation of the recursive definition.

For example, in the recursive function definition

let rec f = fun x ->  f 

the recursive reference to f in the right-hand-side does not need to be evaluated to define the function value fun x -¿ …; since its value will only be required later, when the function is passed an argument. We say that, in this right-hand-side, the mode of use of the variable f is .

In contrast, in the vicious definition let rec x = 1 + x evaluation of the right-hand side 1 + x involves access the value of —x—; we call this usage mode a . Our static check will reject (mutually-)recursive definitions that access a recursive name under this mode.

Some patterns of access fall between the extremes of and . For example, in the cyclic datatype construction let rec ones = 1 :: ones the recursively-bound variable ones appears on right-hand side without being placed inside a function abstraction. However, since it appears in a “guarded” position, directly beneath the value constructor —::—, evaluation only needs to access its address, not its value. We say that the mode of use of the variable ones is .

Finally, a variable —x— may also appear in a position where its value is not inspected, neither is it guarded beneath a constructor, as in the expression x, or let y = x in y, for example. In such cases we say that the value is “returned” directly and use the mode . As with —Dereference—, recursive definitions that access variables at the mode , such as the following let rec x = x, would be under-determined and are rejected.

We also use a last

mode to classify variables that are not used at all in a term.

2.2 A right-to-left inference system

The central contribution of our work is a simple system of inference rules for a judgment of the form , where is a program term, is an access mode, and the environment maps term variables to access modes – modes classify terms and variables, playing the role of types in usual type systems. The example judgment x : , y : (x+1, y) can be read alternatively

left-to-right:

If we know that can safely be used in mode, and can safely be used in mode, then the pair can safely be used under a value constructor (in a -ed context).

right-to-left:

If a context accesses the program fragment under the mode , then this means that the variable is accessed at the mode , and the variable at the mode .

This judgment uses access modes to classify not just variables, but also the constraints imposed to a subterm by its surrounding context. If a context uses its subterm at the mode , then any derivation for will contain a sub-derivation of the form .

In general, we can define a notion of mode composition: if we try to prove , then the sub-derivation will check , where is the composition of the access-mode under a surrounding usage mode , and is neutral for composition.

Our judgment can be directed into an algorithm following our right-to-left interpretation. Given a term and an mode as inputs, our algorithm computes the least demanding environment such that holds.

For example, the inference rule for function abstractions in our system is as follows: Γ, x m_x t m Γx t m The right-to-left reading of the rule is as follows. To compute the constraints on in a context of mode , it suffices to the check the function body under the weaker mode , and remove the function variable from the collected constraints – its mode does not matter. If is just a variable and is , we get the environment as a result.

Given a family of mutually-recursive definitions , we run our algorithm on each at the mode , and obtain a family of environments such that all the judgments hold. The definitions are rejected if one of the contains one of the mutually-defined names under the mode or rather than or .

3 A core language of recursive definitions

Family notation

We write for a family of objects parametrized over an index over finite set . Furthermore, we assume that index sets are totally ordered, so that the elements of the family are traversed in a predetermined linear order; we write for the combined family over , with the indices in ordered before the indices of . When there is no need for precision, we often omit the index set, writing . Our syntax, judgments, and inference rules will often use families: for example, is a mutually-recursive definition of families of terms bound to corresponding variables – assumed distinct, we follow the Barendregt convention. Sometimes a family is used where a term is expected, and the interpretation should be clear: for example, when we say “ holds”, we implicitly use a conjunctive interpretation: each of the judgments in the family holds. Finally, we write for the empty family.

3.1 Syntax

Terms ∋t, u x, y, zb ux tt uK i t_it hBindings ∋b i x_i = t_iHandlers ∋h i p_i t_iPatterns ∋p, q x, y, zK i p_i
Figure 1: Core language syntax

Figure 1 introduces a minimal subset of ML containing the interesting ingredients of OCaml’s recursive values:

  • a multi-ary let rec binding ,

  • functions (-abstractions) to write recursive occurrences whose evaluation is delayed. (OCaml has additional constructs for delaying computation, such as —lazy— values and object literals.)

  • datatype constructors to write (safe) cyclic data structures; these stand in both for user-defined constructors and for built-in types such as lists and tuples

  • shallow pattern-matching

    , to write code that inspects values, in particular code with vicious circles.

The following common ML constructs do not need to be primitive forms, as we can desugar them into our core language. In particular, the full inference rules for OCaml (and our check) exactly correspond to the rules (and check) derived from this desugaring.

  • -ary tuples are a special case of constructor:
    desugars into .

  • Non-recursive let binding are recursive bindings with recursive mode :
    desugars into .

  • Conditionals are a special case of pattern-matching: