ATS is known for its sophisticated type system, but it also has a template system that has been the focus of recent work. Rather than going into technical details or comparing templates to other forms of generic programming, I wanted to give an example.

Code Counting

Suppose we want to count lines of code, à la cloc. Briefly, this means detecting various different programming languages and then examining file contents to count the number of lines, comments, and blanks.

A naïve approach would be to define a type

vtypedef file = @{ lines = int, blanks = int, comments = int, doc_comments = int }

and functions

fn read_file(fp: string) : string

fn count_haskell(file_contents: string) : file

fn count_j(file_contents: string) : file

This is unnecessarily slow, particularly in a code-counting tool like cloc: it will allocate each time a file is opened. Instead, we can define a set of function templates for each programming language that count lines of code from a stream of characters, viz.

fn {a:vt@ype} advance_char$lang (char, &a >> _, &file >> _) : void

fn {a:vt@ype} free$lang (a) : void

fn {a:vt@ype} init$lang (&a? >> a) : void

The workhorse here is advance_char$lang: it takes a character, a reference to a value of type a, and a reference to a value of type file. For each programming language we define a custom parse state a, and implement advance_char$lang for that particular a.

As an example consider the implemenation of advance_char$lang for Egison in SATS/lang/egison.sats:

datavtype parse_state_egi = | line_comment | regular | post_newline_whitespace

fn free_st_egi(parse_state_egi) : void

overload free with free_st_egi

and DATS/lang/egison.dats:

staload "SATS/file.sats" staload "SATS/lang/egison.sats" staload "SATS/lang/common.sats"

implement free_st_egi (st) = case+ st of | ~line_comment() => () | ~regular() => () | ~post_newline_whitespace() => ()

implement free$lang<parse_state_egi> (st) = free_st_egi(st)

implement init$lang<parse_state_egi> (st) = st := post_newline_whitespace

implement advance_char$lang<parse_state_egi> (c, st, file_st) = case+ st of | regular() => begin case+ c of | '\n' => (free(st) ; file_st.lines := file_st.lines + 1 ; st := post_newline_whitespace) | _ => () end | line_comment() => begin case+ c of | '\n' => (free(st) ; file_st.comments := file_st.comments + 1 ; st := post_newline_whitespace) | _ => () end | post_newline_whitespace() => begin case+ c of | '\n' => (file_st.blanks := file_st.blanks + 1) | '\t' => () | ' ' => () | ';' => (free(st) ; st := line_comment) | _ => (free(st) ; st := regular) end

Tying this all together, we define a template function to count lines of code in a file:

#define BUFSZ 32768

staload "libats/libc/SATS/stdio.sats" staload "SATS/file.sats" staload "SATS/lang/common.sats"

fn {a:vt@ype} count_for_loop { l : addr | l != null }{m:nat}{ n : nat | n <= m }( pf : !bytes_v(l, m) | p : ptr(l) , parse_st : &a >> _ , bufsz : size_t(n) ) : file = let var res: file = empty_file var i: size_t val () = for* { i : nat | i <= n } .<n-i>. (i : size_t(i)) => (i := i2sz(0) ; i < bufsz ; i := i + 1) let var current_char = byteview_read_as_char(pf | add_ptr_bsz(p, i)) in advance_char$lang<a>(current_char, parse_st, res) end in res end

fn {a:vt@ype} count_file_buf { l : addr | l != null }(pf : !bytes_v(l, BUFSZ) | p : ptr(l), inp : !FILEptr1) : file = let var init_st: a val () = init$lang<a>(init_st) fun loop { l : addr | l != null }(pf : !bytes_v(l, BUFSZ) | inp : !FILEptr1, st : &a >> _, p : ptr(l)) : file = let var file_bytes = freadc(pf | inp, i2sz(BUFSZ), p) extern praxi lt_bufsz {m:nat} (size_t(m)) : [m <= BUFSZ] void in if file_bytes = 0 then empty_file else let prval () = lt_bufsz(file_bytes) var acc = count_for_loop<a>(pf | p, st, file_bytes) in acc + loop(pf | inp, st, p) end end var ret = loop(pf | inp, init_st, p) val () = free$lang<a>(init_st) in ret end

Notice that we can define the count_file_buf template in terms of advance_char$lang<a>. That is, we provide a generic way to stream characters from a file, and then call count_file_bug<parse_state_egi> (for instance) when we want to count lines of code in an Egison file.

The full setup is available here.

Note

The error messages related to misusing templates are not particularly refined. If, for example, we attempt to call count_file_buf<parse_state_egi> without implementing advance_char$lang<parse_state_egi>, we get

.atspkg/c/src/wc-demo.c:76635:28: error: ‘PMVtmpltcstmat’ undeclared (first use in this function) 76635 | ATSINSmove_void(tmp937__6, PMVtmpltcstmat[0](init$lang<S2Ecst(parse_state_egi)>)(ATSPMVrefarg1(ATSPMVptrof(tmpref936__6)))) ; | ^~~~~~~~~~~~~~ /home/vanessa/.atspkg/0.3.13/lib/ats2-postiats-0.3.13/ccomp/runtime/pats_ccomp_instrset.h:284:39: note: in definition of macro ‘ATSINSmove_void’ 284 | #define ATSINSmove_void(tmp, command) command | ^~~~~~~ .atspkg/c/src/wc-demo.c:76635:28: note: each undeclared identifier is reported only once for each function it appears in 76635 | ATSINSmove_void(tmp937__6, PMVtmpltcstmat[0](init$lang<S2Ecst(parse_state_egi)>)(ATSPMVrefarg1(ATSPMVptrof(tmpref936__6)))) ; | ^~~~~~~~~~~~~~ /home/vanessa/.atspkg/0.3.13/lib/ats2-postiats-0.3.13/ccomp/runtime/pats_ccomp_instrset.h:284:39: note: in definition of macro ‘ATSINSmove_void’ 284 | #define ATSINSmove_void(tmp, command) command | ^~~~~~~ .atspkg/c/src/wc-demo.c:76635:46: error: ‘init$lang’ undeclared (first use in this function) 76635 | ATSINSmove_void(tmp937__6, PMVtmpltcstmat[0](init$lang<S2Ecst(parse_state_egi)>)(ATSPMVrefarg1(ATSPMVptrof(tmpref936__6)))) ; | ^~~~~~~~~ /home/vanessa/.atspkg/0.3.13/lib/ats2-postiats-0.3.13/ccomp/runtime/pats_ccomp_instrset.h:284:39: note: in definition of macro ‘ATSINSmove_void’ 284 | #define ATSINSmove_void(tmp, command) command | ^~~~~~~ .atspkg/c/src/wc-demo.c:76635:56: warning: implicit declaration of function ‘S2Ecst’ [-Wimplicit-function-declaration] 76635 | ATSINSmove_void(tmp937__6, PMVtmpltcstmat[0](init$lang<S2Ecst(parse_state_egi)>)(ATSPMVrefarg1(ATSPMVptrof(tmpref936__6)))) ; | ^~~~~~ /home/vanessa/.atspkg/0.3.13/lib/ats2-postiats-0.3.13/ccomp/runtime/pats_ccomp_instrset.h:284:39: note: in definition of macro ‘ATSINSmove_void’ 284 | #define ATSINSmove_void(tmp, command) command | ^~~~~~~ .atspkg/c/src/wc-demo.c:76635:63: error: ‘parse_state_egi’ undeclared (first use in this function) 76635 | ATSINSmove_void(tmp937__6, PMVtmpltcstmat[0](init$lang<S2Ecst(parse_state_egi)>)(ATSPMVrefarg1(ATSPMVptrof(tmpref936__6)))) ; | ^~~~~~~~~~~~~~~ /home/vanessa/.atspkg/0.3.13/lib/ats2-postiats-0.3.13/ccomp/runtime/pats_ccomp_instrset.h:284:39: note: in definition of macro ‘ATSINSmove_void’ 284 | #define ATSINSmove_void(tmp, command) command | ^~~~~~~ .atspkg/c/src/wc-demo.c:76635:80: error: expected expression before ‘)’ token 76635 | ATSINSmove_void(tmp937__6, PMVtmpltcstmat[0](init$lang<S2Ecst(parse_state_egi)>)(ATSPMVrefarg1(ATSPMVptrof(tmpref936__6)))) ; | ^ /home/vanessa/.atspkg/0.3.13/lib/ats2-postiats-0.3.13/ccomp/runtime/pats_ccomp_instrset.h:284:39: note: in definition of macro ‘ATSINSmove_void’ 284 | #define ATSINSmove_void(tmp, command) command | ^~~~~~~ .atspkg/c/src/wc-demo.c:76651:46: error: ‘free$lang’ undeclared (first use in this function) 76651 | ATSINSmove_void(tmp946__6, PMVtmpltcstmat[0](free$lang<S2Ecst(parse_state_egi)>)(tmpref936__6)) ; | ^~~~~~~~~ /home/vanessa/.atspkg/0.3.13/lib/ats2-postiats-0.3.13/ccomp/runtime/pats_ccomp_instrset.h:284:39: note: in definition of macro ‘ATSINSmove_void’ 284 | #define ATSINSmove_void(tmp, command) command | ^~~~~~~ .atspkg/c/src/wc-demo.c:76651:80: error: expected expression before ‘)’ token 76651 | ATSINSmove_void(tmp946__6, PMVtmpltcstmat[0](free$lang<S2Ecst(parse_state_egi)>)(tmpref936__6)) ; | ^ /home/vanessa/.atspkg/0.3.13/lib/ats2-postiats-0.3.13/ccomp/runtime/pats_ccomp_instrset.h:284:39: note: in definition of macro ‘ATSINSmove_void’ 284 | #define ATSINSmove_void(tmp, command) command | ^~~~~~~ .atspkg/c/src/wc-demo.c: In function ‘count_for_loop_201__201__6’: .atspkg/c/src/wc-demo.c:76944:28: error: ‘PMVtmpltcstmat’ undeclared (first use in this function) 76944 | ATSINSmove_void(tmp934__6, PMVtmpltcstmat[0](advance_char$lang<S2Ecst(parse_state_egi)>)(tmpref932__6, ATSPMVrefarg1(arg1), ATSPMVrefarg1(ATSPMVptrof(tmpref927__6)))) ; | ^~~~~~~~~~~~~~ /home/vanessa/.atspkg/0.3.13/lib/ats2-postiats-0.3.13/ccomp/runtime/pats_ccomp_instrset.h:284:39: note: in definition of macro ‘ATSINSmove_void’ 284 | #define ATSINSmove_void(tmp, command) command | ^~~~~~~ .atspkg/c/src/wc-demo.c:76944:46: error: ‘advance_char$lang’ undeclared (first use in this function) 76944 | ATSINSmove_void(tmp934__6, PMVtmpltcstmat[0](advance_char$lang<S2Ecst(parse_state_egi)>)(tmpref932__6, ATSPMVrefarg1(arg1), ATSPMVrefarg1(ATSPMVptrof(tmpref927__6)))) ; | ^~~~~~~~~~~~~~~~~ /home/vanessa/.atspkg/0.3.13/lib/ats2-postiats-0.3.13/ccomp/runtime/pats_ccomp_instrset.h:284:39: note: in definition of macro ‘ATSINSmove_void’ 284 | #define ATSINSmove_void(tmp, command) command | ^~~~~~~ .atspkg/c/src/wc-demo.c:76944:71: error: ‘parse_state_egi’ undeclared (first use in this function) 76944 | ATSINSmove_void(tmp934__6, PMVtmpltcstmat[0](advance_char$lang<S2Ecst(parse_state_egi)>)(tmpref932__6, ATSPMVrefarg1(arg1), ATSPMVrefarg1(ATSPMVptrof(tmpref927__6)))) ; | ^~~~~~~~~~~~~~~ /home/vanessa/.atspkg/0.3.13/lib/ats2-postiats-0.3.13/ccomp/runtime/pats_ccomp_instrset.h:284:39: note: in definition of macro ‘ATSINSmove_void’ 284 | #define ATSINSmove_void(tmp, command) command | ^~~~~~~ .atspkg/c/src/wc-demo.c:76944:88: error: expected expression before ‘)’ token 76944 | ATSINSmove_void(tmp934__6, PMVtmpltcstmat[0](advance_char$lang<S2Ecst(parse_state_egi)>)(tmpref932__6, ATSPMVrefarg1(arg1), ATSPMVrefarg1(ATSPMVptrof(tmpref927__6)))) ; | ^ /home/vanessa/.atspkg/0.3.13/lib/ats2-postiats-0.3.13/ccomp/runtime/pats_ccomp_instrset.h:284:39: note: in definition of macro ‘ATSINSmove_void’ 284 | #define ATSINSmove_void(tmp, command) command | ^~~~~~~ )