Bluish Coder

Programming Languages, Martials Arts and Computers. The Weblog of Chris Double.


2018-01-02

Casting in ATS

The ATS programming language has a powerful type and proof system to enable safe construction of programs. Sometimes there is a need to cast a value from one type to another. ATS provides a feature that enables the programming to write their own rules specifying what can be cast. This becomes important when converting types from non-dependent typed values to dependantly typed values and dealing with conversion of proofs. This post goes through some common examples.

A casting function is introduced using the keyword castfn. It only has a declaration - there is no implementation or body of the casting function. From a value perspective there is no change to the actual value being cast, only to the type. It is effectively the identity function except for the type change. This means the cast is only allowed for types that are the same size, typically a machine word. There is no runtime overhead for using a casting function.

Standard Types

The following example ATS program attempts to add an integer to an unsigned integer value:

#include "share/atspre_define.hats"
#include "share/atspre_staload.hats"

implement main0() = let
  val x: uint = 5u
  val y: int = x + 5
in
  println!("y=", y)
end 

This results in a type error:

$ patscc -o c2 c2.dats
c2.dats: 133(line=6, offs=16) -- 138(line=6, offs=21):
         error(3): the symbol [+] cannot be resolved as no match is found.
c2.dats: 124(line=6, offs=7) -- 130(line=6, offs=13):
         error(3): the pattern cannot be given the ascribed type.
c2.dats: 124(line=6, offs=7) -- 130(line=6, offs=13):
         error(3): mismatch of static terms (tyleq):
The actual term is: S2Eerrexp()
The needed term is: S2Eapp(S2Ecst(g0int_t0ype); S2Eextkind(atstype_int))

The error is saying that the operator + has no matching implementation for the types provided. It is expecting an int but got something else.

The addition operator requires the two arguments to be of the same type. A casting function can be added to cast the unsigned integer to an integer:

#include "share/atspre_define.hats"
#include "share/atspre_staload.hats"

extern castfn int_of_uint(x: uint): int

implement main0() = let
  val x: uint = 5u
  val y: int = int_of_uint(x) + 5
in
  println!("y=", y)
end

This compiles and runs:

$ patscc -o c3 c3.dats
$ ./c3
y=10

A castfn looks very much like a fun declaration. It requires the name of the casting function, the argument it expects and a return type. There is no implementation as mentioned earlier. The compiler will convert the type of the argument to that of the return type. Another example follows showing conversion to a floating point value:

#include "share/atspre_define.hats"
#include "share/atspre_staload.hats"

extern castfn double_of_uint(x: uint): double

implement main0() = let
  val x: uint = 5u
  val y: double = double_of_uint(x) + 5.0
in
  println!("y=", y)
end

The castfn can have any name but most of the ATS prelude follows a format of including the 'from' and 'to' types in the name.

Dependent Types

Conversion between dependently typed values and non-dependently typed values are a common use of casting functions. For example:

val x: [n:int] size_t n = string1_length("hello")
val y: int = x + 1 // Error here, mismatch between dependent and non-dependent int

The type of x is dependently typed in that it has a type index of sort int for the length of the string. It is also of type size_t but the snippet expects an int. A castfn allows casing away the dependent type index and converting the type at the same time:

 #include "share/atspre_define.hats"
 #include "share/atspre_staload.hats"

 extern castfn int0_of_size1 {n:int} (x: size_t n): int

 implement main0() = let
   val x: [n:int] size_t n = string1_length("hello")
   val y: int = int0_of_size1(x) + 1
 in
   println!("y=", y)
 end 

Note the use of the '0' and '1' suffixes used for the type names in the castfn. This is an idiom in ATS naming where a '0' means not dependently typed and '1' means it has a dependent type.

It's possible to go in the other direction, casting from a non-dependent typed value to a dependently type value. This is an example of code that won't compile and needs a cast:

val x: int = 5
val y: [n:int] size_t n = x

This fails due to x and y being different types. Adding a cast function to convert the int to a size_t and including a type index will allow it to compile:

#include "share/atspre_define.hats"
#include "share/atspre_staload.hats"

extern castfn size1_of_int0 (x: int): [n:int] size_t n

implement main0() = let
  val x: int = 5
  val y: [n:int] size_t n = size1_of_int0(x)
in
  println!("y=", y)
end

Here the castfn adds an existential variable to the return type (the '[n:int]' part) to attach a type index of "unspecified integer value' to the type.

Another common use of casting functions is to convert different string types. A string is a non dependently typed string and a string1 is a dependently typed string with the length of the string as a type index. For example, arguments on a command line are string, but if a string function is called on it then there is a type error:

implement main0(argc, argv) =
  if argc > 1 then let
      val s: string = argv[1]
      val n = string1_length(s)
    in
      ()
    end
  else
    ()

Using a castfn can convert the string to a string1:

#include "share/atspre_define.hats"
#include "share/atspre_staload.hats"

extern castfn string1_of_string (s: string): [n:int] string n

implement main0(argc, argv) =
  if argc > 1 then let
      val s: string = argv[1]
      val n = string1_length(string1_of_string(s)) 
    in
      ()
    end
  else
    ()

Casting linear types

Types can be cast to and from linear types. When programming with strings in ATS you often deal with string which does not need to be manually free'd and strptr which is a linear type and needs to be manually free'd. Sometimes you want to treat a strptr as a string to call a string based function. An example is:

#include "share/atspre_define.hats"
#include "share/atspre_staload.hats"

extern fun strdup(s: string): [l:addr] strptr l = "ext#"
extern castfn strptr_to_string {l:addr} (s: !strptr l): string

implement main0() = let
  val s: [l:addr] strptr l = strdup("hello world")
  val n = string0_length(strptr_to_string(s))
  val _ = strptr_free(s)
in
  ()
end

This example needs to be compiled passing '-DATS_MEMALLOC_LIBC' to tell ATS to use the standard C malloc/free calls.

$ patscc -DATS_MEMALLOC_LIBC -o c8 c8.dats

For the purposes of this example I use a FFI function to call the C strdup function to copy a static string to a linear string that needs to be free'd. Note that the castfn does not consume this linear type (evidenced by the '!' prefixing the type name in the argument). This is dangerous because we can store the return value of the cast and use it after the free:

val oops = strptr_to_string(s)
val _ = strptr_free(s)
val _ = println!(oops)

The danger can be restricted by using locally scoped cast functions:

val n = string0_length(strptr_to_string(s)) where {
          extern castfn strptr_to_string {l:addr} (s: !strptr l): string
        }

Now strptr_to_string can only be used within that local block of code. Cast functions can be dangerous so need some care. There are various prelude casting functions for performing these types of conversions already.

Prelude unsafe casts

The prelude has an 'unsafe' module with a casting polymorphic function which can be used instead of defining casting functions for trivial things. There are also functions for converting strptr to string, etc. The equivalent of the previous example would be:

#include "share/atspre_define.hats"
#include "share/atspre_staload.hats"

staload UN = $UNSAFE

extern fun strdup(s: string): [l:agz] strptr l = "ext#"

implement main0() = let
  val s: [l:agz] strptr l = strdup("hello world")
  val n = string0_length($UN.strptr2string(s))
  val _ = strptr_free(s)
in
  ()
end

Basic type casting can be done using the cast function:

#include "share/atspre_define.hats"
#include "share/atspre_staload.hats"

staload UN = $UNSAFE

implement main0() = let
  val x: uint = 5u
  val y: int = $UN.cast{int}(x) + 5
in
  println!("y=", y)
end 

The swiss army knife of unsafe casting functions are castvwtp0 and castvwtp1. They can be used for casting linear types. The string example previously can use this:

val s: [l:agz] strptr l = strdup("hello world")
val n = string0_length($UN.castvwtp1{string}(s))
val _ = strptr_free(s)

castvwtp1 will not consume the input argument whereas castvwtp0 will. Looking through the prelude/SATS/unsafe.sats file will give an idea of the range of available functions.

The examples in this post are somewhat contrived in that there are standard prelude functions for doing most everything already. The need to cast string to string1 and strptr are reduced due to specific functions being available for those types. However it is useful to learn to use the castfn functionality and it is handy for one-off local casts, or defining protocols using proofs to ensure safe casting to and from types.

Tags


This site is accessable over tor as hidden service mh7mkfvezts5j6yu.onion, or Freenet using key:
USK@1ORdIvjL2H1bZblJcP8hu2LjjKtVB-rVzp8mLty~5N4,8hL85otZBbq0geDsSKkBK4sKESL2SrNVecFZz9NxGVQ,AQACAAE/bluishcoder/-44/


Tags

Archives
Links