Compared with Rust, ATS provides better type safety at the boundaries; foreign functions and data structures can be used in a first-class way without sacrificing type safety.

This is due to ATS' viewtypes; integer-indexed/dependent linear types let us work safely with pointers.

As an example, we can safely call into a Futhark-generated API.

We use the mean function here; we export mean_f32 from Futhark, viz.

import "lib/github.com/diku-dk/statistics/statistics"

module stats\_f32 = mk\_statistics f32

entry mean\_f32 = stats\_f32.mean

The Futhark compiler then generates the follow C header files:

struct futhark\_context\_config ;
struct futhark\_context\_config \*futhark\_context\_config\_new(void);
void futhark\_context\_config\_free(struct futhark\_context\_config \*cfg);

struct futhark\_context ;
struct futhark\_context \*futhark\_context\_new(struct futhark\_context\_config \*cfg);
void futhark\_context\_free(struct futhark\_context \*ctx);

struct futhark\_f32\_1d ;
struct futhark\_f32\_1d \*futhark\_new\_f32\_1d(struct futhark\_context \*ctx,
                                          float \*data, int64\_t dim0);
int futhark\_free\_f32\_1d(struct futhark\_context \*ctx,
                        struct futhark\_f32\_1d \*arr);

int futhark\_entry\_mean\_f32(struct futhark\_context \*ctx, float \*out0, const
                           struct futhark_f32_1d *in0);

There are some helper functions for allocating and freeing Futhark arrays.

The corresponding .sats file is:

absvt@ype fut\_ctx\_cfg
absvt@ype fut\_ctx
absvt@ype f32\_arr\_1d

vtypedef futctxcfgptr = [ l : addr | l > null ] (fut\_ctx\_cfg @ l | ptr(l))
vtypedef futctxptr = [ l : addr | l > null ] (fut\_ctx @ l | ptr(l))

absvt@ype f32\_arr(m: int)

vtypedef f32\_arrptr(m: int) = [ l : addr | l > null ] (f32\_arr(m) @ l | ptr(l))

// technically should be int not int64 but nothing bad will happen on 64-bit architectures
fn futhark\_new\_f32\_1d {m:nat}(p : !futctxptr, inp : !arrayptr(float, m), int(m)) : f32\_arrptr(m) =
  "ext#"

fn futhark\_free\_f32\_1d {m:nat}(p : !futctxptr, arr : f32\_arrptr(m)) : int =
  "ext#"

fn futhark\_entry\_mean\_f32 {m:nat}(p : !futctxptr, out : (&float? >> \_), inp : !f32\_arrptr(m)) : int =
  "ext#"

This API is safe; not only does it prevent double-frees with linear types, its use of integer-indexed refinement types with pointers ensures that we do not access past the end of the array even while we work with "raw" pointers.

Consider:

implement main0 () =
  {
    val arr0 = arrayptr($arrpsz{float}(1.0f, 2.0f, 3.0f))
    val ctx\_cfg = futhark\_context\_config\_new()
    val ctx = futhark\_context\_new(ctx\_cfg)
    val fut\_arr0 = futhark\_new\_f32\_1d(ctx, arr0, 3)
    var ret: float
    val \_ = futhark\_entry\_mean\_f32(ctx, ret, fut\_arr0)
    val () = println!(ret)
    val \_ = futhark\_free\_f32\_1d(ctx, fut\_arr0)
    val () = futhark\_context\_free(ctx)
    val () = futhark\_context\_config\_free(ctx\_cfg)
    val () = arrayptr\_free(arr0)
  }

If we replace

    val arr0 = arrayptr($arrpsz{float}(1.0f, 2.0f, 3.0f))
    ⋮
    val fut_arr0 = futhark_new_f32_1d(ctx, arr0, 3)

with

    val arr0 = arrayptr($arrpsz{float}(1.0f, 2.0f, 3.0f))
    ⋮
    val fut_arr0 = futhark_new_f32_1d(ctx, arr0, 2)

the program would fail to compile because arr0 has length 3 but 2 is not the size of the array arr0.

You can see the full example (an ATS library for GPU-accelerated statistics) here.

In this way, we can work with "raw" pointers via a safe API; this makes ATS unique even among languages like Rust.