In computing, ATS (Applied Type System) is a programming language designed by Hongwei Xi to unify programming with formal specification. ATS has support for combining theorem proving with practical programming through the use of advanced type systems.[2] A past version of The Computer Language Benchmarks Game has demonstrated that the performance of ATS is comparable to that of the C and C++ programming languages.[3] By using theorem proving and strict type checking, the compiler can detect and prove that its implemented functions are not susceptible to bugs such as division by zero, memory leaks, buffer overflow, and other forms of memory corruption by verifying pointer arithmetic and reference counting before the program compiles. Additionally, by using the integrated theorem-proving system of ATS (ATS/LF), the programmer may make use of static constructs that are intertwined with the operative code to prove that a function conforms to its specification.
Paradigm | multi-paradigm: functional, imperative |
---|---|
Designed by | Hongwei Xi at Boston University |
Stable release | ATS2-0.4.2[1]
/ November 14, 2020 |
Typing discipline | static |
License | GPLv3 |
Filename extensions | .sats, .dats, .hats |
Website | http://www.ats-lang.org/ |
Influenced by | |
Dependent ML, ML, OCaml, C++ |
ATS consists of a static component and a dynamic component. The static component is used for handling types, whereas the dynamic component is used for programs. While ATS primarily relies on a call-by-value functional language at its core, it possesses the capability to accommodate diverse programming paradigms, such as functional, imperative, object-oriented, concurrent, and modular programming.
According to the author, ATS was inspired by Martin-Löf's constructive type theory, which was originally developed for the purpose of establishing a foundation for mathematics. Xi designed ATS “in an attempt to combine specification and implementation into a single programming language.” [4]
ATS is derived mostly from the ML and OCaml programming languages. An earlier language, Dependent ML, by the same author has been incorporated by the language.
The first implementation, ATS/Proto (ATS0), was written in OCaml and was released in 2006. This was the pre-first edition of ATS and is no longer maintained. A year later, ATS/Geizella, the first implementation of ATS1, was released. This version was also written in OCaml and is not actively used anymore. [5]
The second version of ATS1, ATS/Anairiats, released in 2008, was a major milestone in the development of the language, as the language was able to bootstrap itself. This version was written almost completely in ATS1. The current version, ATS/Postiats (ATS2) was released in 2013. Like its predecessor, this version is also almost entirely written in ATS1. The most recently released version is ATS2-0.4.2. [5]
Currently, ATS is mostly only used for research; there are less than 200 GitHub repositories containing code written in ATS. This is much less than other functional programming languages, such as OCaml and Standard ML, which have over 16000 and 3000 repositories, respectively. This is probably due to the steep learning curve associated with ATS, which is present because of the language’s use of dependent type-checking and template instance resolution. These features usually require the use of explicit quantifiers, which raises the learning curve. [6]
ATS/Xanadu (ATS3) is currently actively being developed in ATS2, with the hope of lowering this learning curve by two main improvements:
With these improvements, Xi hopes for ATS to become much more accessible and easier to learn. In fact, the primary goal of ATS3 is to transform ATS from a programming language mainly used for research into one strong enough for large-scale industrial software development. [5]
The primary focus of ATS is to support theorem proving in combination with practical programming.[2] With theorem proving one can prove, for instance, that an implemented function does not produce memory leaks. It also prevents other bugs that might otherwise only be found during testing. It incorporates a system similar to those of proof assistants which usually only aim to verify mathematical proofs—except ATS uses this ability to prove that the implementations of its functions operate correctly, and produce the expected output.
As a simple example, in a function using division, the programmer may prove that the divisor will never equal zero, preventing a division by zero error. Let's say, the divisor 'X' was computed as 5 times the length of list 'A'. One can prove, that in the case of a non-empty list, 'X' is non-zero, since 'X' is the product of two non-zero numbers (5 and the length of 'A'). A more practical example would be proving through reference counting that the retain count on an allocated block of memory is being counted correctly for each pointer. Then one can know, and quite literally prove, that the object will not be deallocated prematurely, and that memory leaks will not occur.
The benefit of the ATS system is that since all theorem proving occurs strictly within the compiler, it has no effect on the speed of the executable program. ATS code is often harder to compile than standard C code, but once it compiles the programmer can be certain that it is running correctly to the degree specified by their proofs (assuming the compiler and runtime system are correct).
In ATS proofs are separate from implementation, so it is possible to implement a function without proving it if the programmer so desires.
According to the author, ATS's efficiency[7] is largely due to the way that data is represented in the language and tail-call optimizations (which are generally important for the efficiency of functional programming languages). Data can be stored in a flat or unboxed representation rather than a boxed representation.
dataprop
expresses predicates as algebraic types.
Predicates in pseudo‑code somewhat similar to ATS source (see below for valid ATS source):
FACT(n, r) iff fact(n) = r MUL(n, m, prod) iff n * m = prod FACT(n, r) = FACT(0, 1) | FACT(n, r) iff FACT(n-1, r1) and MUL(n, r1, r) // for n > 0 // expresses fact(n) = r iff r = n * r1 and r1 = fact(n-1)
In ATS code:
dataprop FACT (int, int) =
| FACTbas (0, 1) // basic case: FACT(0, 1)
| {n:int | n > 0} {r,r1:int} // inductive case
FACTind (n, r) of (FACT (n-1, r1), MUL (n, r1, r))
where FACT (int, int) is a proof type
Non tail-recursive factorial with proposition or "Theorem" proving through the construction dataprop.
The evaluation of fact1(n-1)
returns a pair (proof_n_minus_1 | result_of_n_minus_1)
which is used in the calculation of fact1(n)
. The proofs express the predicates of the proposition.
[FACT (n, r)] implies [fact (n) = r] [MUL (n, m, prod)] implies [n * m = prod]
FACT (0, 1) FACT (n, r) iff FACT (n-1, r1) and MUL (n, r1, r) forall n > 0
To remember:
{...} universal quantification [...] existential quantification (... | ...) (proof | value) @(...) flat tuple or variadic function parameters tuple .<...>. termination metric[8]
#include "share/atspre_staload.hats"
dataprop FACT (int, int) =
| FACTbas (0, 1) of () // basic case
| {n:nat}{r:int} // inductive case
FACTind (n+1, (n+1)*r) of (FACT (n, r))
(* note that int(x) , also int x, is the monovalued type of the int x value.
The function signature below says:
forall n:nat, exists r:int where fact( num: int(n)) returns (FACT (n, r) | int(r)) *)
fun fact{n:nat} .<n>. (n: int (n)) : [r:int] (FACT (n, r) | int(r)) =
(
ifcase
| n > 0 => ((FACTind(pf1) | n * r1)) where
{
val (pf1 | r1) = fact (n-1)
}
| _(*else*) => (FACTbas() | 1)
)
implement main0 (argc, argv) =
{
val () = if (argc != 2) then prerrln! ("Usage: ", argv[0], " <integer>")
val () = assert (argc >= 2)
val n0 = g0string2int (argv[1])
val n0 = g1ofg0 (n0)
val () = assert (n0 >= 0)
val (_(*pf*) | res) = fact (n0)
val ((*void*)) = println! ("fact(", n0, ") = ", res)
}
This can all be added to a single file and compiled as follows. Compilation should work with various back end C compilers, e.g. gcc. Garbage collection is not used unless explicitly stated with -D_ATS_GCATS
)[9]
$ patscc fact1.dats -o fact1
$ ./fact1 4
compiles and gives the expected result
val x : @(int, char) = @(15, 'c') // x.0 = 15 ; x.1 = 'c'
val @(a, b) = x // pattern matching binding, a= 15, b='c'
val x = @{first=15, second='c'} // x.first = 15
val @{first=a, second=b} = x // a= 15, b='c'
val @{second=b, ...} = x // with omission, b='c'
val x : '(int, char) = '(15, 'c') // x.0 = 15 ; x.1 = 'c'
val '(a, b) = x // a= 15, b='c'
val x = '{first=15, second='c'} // x.first = 15
val '{first=a, second=b} = x // a= 15, b='c'
val '{second=b, ...} = x // b='c'
With '|' as separator, some functions return wrapped the result value with an evaluation of predicates
val ( predicate_proofs | values) = myfunct params
{...} universal quantification [...] existential quantification (...) parenthetical expression or tuple (... | ...) (proofs | values)
.<...>. termination metric @(...) flat tuple or variadic function parameters tuple (see example's printf) @[byte][BUFLEN] type of an array of BUFLEN values of type byte[10] @[byte][BUFLEN]() array instance @[byte][BUFLEN](0) array initialized to 0
sortdef nat = {a: int | a >= 0 } // from prelude: ∀ a ∈ int ...
typedef String = [a:nat] string(a) // [..]: ∃ a ∈ nat ...
// {..}: ∀ a,b ∈ type ... fun {a,b:type} swap_type_type (xy: @(a, b)): @(b, a) = (xy.1, xy.0)
T @ L
asserts that there is a view of type T at location L fun {a:t@ype} ptr_get0 {l:addr} (pf: a @ l | p: ptr l): @(a @ l | a)
fun {a:t@ype} ptr_set0 {l:addr} (pf: a? @ l | p: ptr l, x: a): @(a @ l | void)
ptr_get0 (T)
is ∀ l : addr . ( T @ l | ptr( l ) ) -> ( T @ l | T) // see manual, section 7.1. Safe Memory Access through Pointers
[12]viewdef array_v (a:viewt@ype, n:int, l: addr) = @[a][n] @ l
as in case+, val+, type+, viewtype+, ...
staload "foo.sats" // foo.sats is loaded and then opened into the current namespace staload F = "foo.sats" // to use identifiers qualified as $F.bar dynload "foo.dats" // loaded dynamically at run-time
Dataviews are often declared to encode recursively defined relations on linear resources.[13]
dataview array_v (a: viewt@ype+, int, addr) = | {l: addr} array_v_none (a, 0, l) | {n: nat} {l: addr} array_v_some (a, n+1, l) of (a @ l, array_v (a, n, l+sizeof a))
Datatypes[14]
datatype workday = Mon | Tue | Wed | Thu | Fri
lists
datatype list0 (a:t@ype) = list0_cons (a) of (a, list0 a) | list0_nil (a)
A dataviewtype is similar to a datatype, but it is linear. With a dataviewtype, the programmer is allowed to explicitly free (or deallocate) in a safe manner the memory used for storing constructors associated with the dataviewtype.[15]
local variables
var res: int with pf_res = 1 // introduces pf_res as an alias of view @ (res)
on stack array allocation:
#define BUFLEN 10 var !p_buf with pf_buf = @[byte][BUFLEN](0) // pf_buf = @[byte][BUFLEN](0) @ p_buf[16]
See val and var declarations[17]