- + - + - + - + A x

Notes on using Sesterl

06 Jun 2021 (last edited on 20 Dec 2021)

Sesterl is a new statically typed programming language for the BEAM (the Erlang virtual machine).

Installation

To install Sesterl, we can download a release build for Ubuntu or MacOS from the Releases page on the Sesterl GitHub Repository.

$ unzip sesterl-0-2-1-ubuntu-latest.zip
Archive:  sesterl-0-2-1-ubuntu-latest.zip
  inflating: sesterl
$ chmod +x sesterl
$ mv sesterl ~/.local/bin/
$ sesterl --version
v0.2.1

On other systems sesterl has to be compiled from source.

“Hello World” with rebar3

Sesterl can generate a rebar3 config file for a project from a sesterl.yaml file.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
# REQUIRED CONFIG # Package name is used to derive a prefix for resulting Erlang modules, # e.g. this will cause all modules to be prefixed with "HelloSesterl." package: "hello_sesterl" # Main module is the only interface to a package from outside world. # It has to exist and can't be imported by modules in that package. main_module: "Hello" source_directories: - "src" # OPTIONAL CONFIG (default values shown) # test_directories: [] # dependencies: [] # test_dependencies: [] # erlang: # output_directory: "_generated" # test_output_directory: "_generated_test"
sesterl.yaml

To generate a rebar3 config from the above file, we can run:

$ sesterl config .
  output written on '/home/michal/hello_sesterl/./rebar.config'.

A rebar3 project also needs an *.app.src file in the src/ directory:

1 2 3 4 5 6 7 8
{application, hello_sesterl, [{description, "An OTP library"}, {vsn, "0.1.0"}, {applications, [kernel, stdlib ]} ]}.
src/hello_sesterl.app.src

Sesterl source files have a .sest file extension. The syntax is similar to Standard ML or OCaml.

1 2 3 4 5 6
module Hello = struct val my_hello() = print_debug("Hello, world!") end
src/some_file.sest

To compile a project, we can run:

$ rebar3 do sesterl compile, compile
===> Fetching rebar_sesterl (from {git,"https://github.com/gfngfn/rebar_sesterl_plugin.git",
                         {branch,"master"}})
===> Analyzing applications...
===> Compiling rebar_sesterl
===> Verifying dependencies...
===> Compiling Sesterl programs (command: "sesterl build ./ -o _generated") ...
  parsing '/home/michal/projects/hello_sesterl/src/some_file.sest' ...
  type checking '/home/michal/projects/hello_sesterl/src/some_file.sest' ...
  output written on '/home/michal/projects/hello_sesterl/./_generated/HelloSesterl.Hello.erl'.
  output written on '/home/michal/projects/hello_sesterl/./_generated/sesterl_internal_prim.erl'.
===> Analyzing applications...
===> Compiling hello_sesterl
_generated/sesterl_internal_prim.erl:8:14: Warning: variable 'Arity' is unused

===> Verifying dependencies...
===> Analyzing applications...
===> Compiling hello_sesterl
_generated/sesterl_internal_prim.erl:8:14: Warning: variable 'Arity' is unused

And to execute code from the above module from the Erlang shell:

$ rebar3 shell
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling hello_sesterl
Erlang/OTP 24 [erts-12.0.2] [source] [64-bit] [smp:6:6] [ds:6:6:10] [async-threads:1] [jit]

Eshell V12.0.2  (abort with ^G)
1> 'HelloSesterl.Hello':my_hello().
<<"Hello, world!">>
ok

Yay!

Sesterl standard library

Sesterl standard library is just a rebar3 package, that can be added as a git dependency to the configuration yaml file:

1 2 3 4 5 6 7 8
dependencies: - name: "sesterl_stdlib" source: type: "git" repository: "https://github.com/gfngfn/sesterl_stdlib" spec: type: "branch" value: "master"

Rebar3 config needs to be regenerated with sesterl config ., and on next compilation we’ll be able to refer to modules from the stdlib (but only through what is defined in its main Stdlib module):

1 2 3 4 5
module Hello = struct val my_hello() = let list = Stdlib.Binary.to_list("Hello, world!") in print_debug(list) end

1 2 3 4 5 6 7 8
module Hello = struct module Binary = Stdlib.Binary val my_hello() = let list = Binary.to_list("Hello, world!") in print_debug(list) end

1 2 3 4 5 6 7 8
module Hello = struct open Stdlib.Binary val my_hello() = let list = to_list("Hello, world!") in print_debug(list) end

Bonus: “Hello World” without rebar3

1 2 3 4 5 6 7 8 9 10 11
module Hello = struct val print_string : fun(binary) -> unit = external 1 ``` print_string(S) -> io:format("~ts~n", [S]). ``` val main(args) = print_string("Hello, world!") end
without_rebar.sest

When compiling the above example with sesterl, we get two Erlang source files as a result:

$ sesterl build without_rebar3.sest -o _generated
  parsing '/home/michal/hello_sesterl/without_rebar3.sest' ...
  type checking '/home/michal/hello_sesterl/without_rebar3.sest' ...
  output written on '/home/michal/hello_sesterl/_generated/Hello.erl'.
  output written on '/home/michal/hello_sesterl/_generated/sesterl_internal_prim.erl'.
$ ls _generated
Hello.erl  sesterl_internal_prim.erl

The Hello.erl file contains the 'Hello' erlang module, and sesterl_internal_prim.erl (“prim” as in “primitives”) contains a module with a few functions to provide some of basic functionality of the language (e.g. Erlang’s send wrapped in a function). This module will not be available in our escript program unless we add code to load it.

To make the escript executable we have to add one dummy line to the beginning of the erlang source file, and then we can run it with escript -c (the -c argument tells escript to compile the module first):

$ (echo "% additional line for escript to work" && cat _generated/Hello.erl) > tmpfile && mv tmpfile _generated/Hello.erl
$ escript -c _generated/Hello.erl
_generated/Hello.erl:7:8: Warning: variable 'S11Args' is unused
%    7|   
%     |   ^^^^^^

Hello, world!

Hurray!

Separate types without tags

Something similar to “newtypes” or phantom types (?) - separate, incompatible types that have the same run-time representation (without boxing or tagging) - can be achieved with the Sesterl’s type system (just like OCaml’s):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
module Hello = struct module Username :> sig type t :: o val from_binary : fun(binary) -> t end = struct type t = binary val from_binary(x) = x end module Hostname :> sig type t :: o val from_binary : fun(binary) -> t end = struct type t = binary val from_binary(x) = x end val do_something_with_username(x : Username.t) = x val main() = let a = Hostname.from_binary("example.com") in do_something_with_username(a) end /*(* ! [Type error] file 'some_file.sest', line 23, characters 31-32: this expression has type Hello.Hostname.t but is expected of type Hello.Username.t *)*/

Or if we want to reuse the interface signature and implementation:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
module Hello = struct signature ID = sig type t :: o val from_binary : fun(binary) -> t end module BinaryID = struct type t = binary val from_binary(x) = x end module Username :> ID = BinaryID module Hostname :> ID = BinaryID val do_something_with_username(x : Username.t) = x val main() = let a = Hostname.from_binary("example.com") in do_something_with_username(a) end /*(* ! [Type error] file 'some_file.sest', line 22, characters 31-32: this expression has type Hello.Hostname.t but is expected of type Hello.Username.t *)*/

The below example doesn’t work (doesn’t throw an error), because Sesterl ignores the unused parameter from the type and assumes they are the same thing:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
module Hello = struct type id<$a> = binary type hostname = | Hostname type username = | Username val do_something_with_username(x : id<username>) = x val hostname_from_binary(x : binary) : id<hostname> = x val main() = let a = hostname_from_binary("example.com") in do_something_with_username(a) end /*(* No error even though we'd like to see one here! *)*/