Programs and modules - the Blech compilation units

Modules and programs - proposal for Blech compilation units.

The modules proposal is work in progress.

Introduction to compilation units

A compilation unit in Blech is a single file. A file can either be a complete Blech program or a module. A module can be imported to support the implementation of another module or a program.

Program files

A program file is a Blech code file (a .blc-file) that needs exactly one activity marked as an @[EntryPoint]. This activity is the main routine of a Blech program. The name of this entry point to your program can be choosen freely. A program can use other Blech files for its implementation. A program is always a top-level file of a Blech application.

Module files

A module file is a Blech code file that is supposed to be used for the implementation of other modules or programs.

By default, all internals of a Blech code file are hidden. In order to become a module, a Blech code file must be marked as a module and export at least one top-level declaration or a wildcard, see below. TODO: Add a relref here, how?.

Different to other languages we do not use accessibility modifiers in front of declarations. Instead, we require an explicit declaration of exported top-level declarations.

module exposes TopLevelIdentifier

Every top-level declaration inside a module file can be exported to be used by another module or program file. All neccessary declared identifiers must also be exported. Optionally, the exported identifiers can be classified for readability.

module exposes T, MyActivity

type T = int32

function f(x: T) ... end

function MyActivity(x: T) 
    f(x)
    ...
end

In this example the export of activity MyActivity also requires the export of type alias T. The compiler checks the correctness of exports. It is a compiler error, if type T is not exported. For a correct module file an interface file is created that contains all compile-time information that is necessary to compile a Blech file that uses this module file.

signature

type T = int32

function MyActivity(x: T)

Reusing a Blech file

In order to reuse a blech module file it needs to be imported. Only exported entities can be used. The internal - non-exposed - entities are hidden.

Importing a Blech module file for use in a program or another module

In order to use a module file it needs to be imported by another compilation unit, for example a Blech program.

import m = "mymodule"

@[EntryPoint]
activity MyMain()
    run m.MyActivity()
end

Again, "mymodule" resembles the file name of the module. In order order to use it we declare a Blech identifier for the module. It is fine to use any valid identifier, here m. By default all imports are qualified with this module name. In order to use an imported name without qualification is must be exposed at the import.

import m = "mymodule" exposes MyActivity

@[EntryPoint]
activity MyMain()
    var t: m.T 
    run MyActivity(t)
end

Note: The other exported entity – type T – can only be used, when qualifed with the module name.

For exposing everything from an imported module, which is usually neither necessary nor recommended, you can use the short cut ....

import _ = "mymodule" exposes ...

@[EntryPoint]
activity MyMain()
    var t: T 
    run MyActivity(t)
end

In this case, a wildcard _ for the module name can be used.

Usually, several import declarations are necessary for a module or program file.

No implicit export for imported modules

Assume the following two modules.

Module file pair.blc only exports type alias Pair.

module exposes Pair

type Pair = [2]int32

function fst(p: Pair) return int32
    return p[0]
end

function snd(p: Pair) returns int32
    return p[1]
end

Module file usepair.blc imports modul pair, but cannot access hidden functions fst and snd.

import pair = "pair"

module exposes sum

function snd(p: Pair) return int32
    return p[2]
end

function sum(p: pair.Pair) returns int32
    return p[0] + snd(p)
end

On successful compilation the compiler generates the following signature - the interface of a module - in file pair.blh

signature

type Pair = [2]int32

Module "usepair" requires an import of module "pair" and exports function sum.

import pair = "pair"

signature

function sum(p: pair.Pair) returns int32

For reuse, a program that imports module file "usepair" also needs to import module file "pair".

import up = "usepair"
import p = "pair"

@[EntryPoint]
activity Main()
    var p: p.Pair
    _ = up.sum(p)
    await true
end

Note: Module "pair" is not exported indirectly via the import of module "usepair".

The following program cannot be compiled.

import up = "usepair"

@[EntryPoint]
activity Main()
    var p: up.pair.Pair
              ^^^^--- unknown identifier
    _ = up.sum(p)
    await true
end

Exporting nothing

For development purposes it might be useful to expose nothing until a module can be used. A wildcard _ can be used for this purpose.

module exposes _

type Pair = [2]int32

function fst(p: Pair) returns int32
    return p[0]
end

function snd(p: Pair) returns int32
    return p[1]
end

Exporting everything

Sometimes it might be useful to expose everything in a module. There is a shortcut ... for this.

module exposes ...

type Pair = [2]int32

function fst(p: Pair) returns int32
    return p[0]
end

function snd(p: Pair) returns int32
    return p[1]
end

Exporting abstract types as new types

Sometimes it is useful to hide the implementation of a type, from its importers. You can create a newtype for this purpose.

module exposes Pair, set, fst, snd

newtype Pair = [2]int32

function set(fst: int32, snd: int32) returns Pair
    return {fst, snd}
end

function fst(p: Pair) returns int32
    return p[0]
end

function snd(p: Pair) returns int32
    return p[1]
end

Since the internal structure is unknown additional functions must be exposed. The module’s signature does not expose the internal type structure.

signature

newtype Pair

function set(fst: int32, snd: int32) returns Pair

function fst(p: Pair) returns int32

function snd(p: Pair) returns int32

Non-cyclic import hierarchy

The compiler takes care for non-cyclic import dependencies. Cyclic-import dependencies are flaged as a dependency error.

In order to compile a program or module file, every imported module is compiled recursively, if necessary. Every imported module only needs to be loaded once.

File structure

Compiling a Blech module

The compilation of a Blech module file, for example module.blc, generates the following files:

  • A Blech signature file module.blh,
  • a C header file module.h, and
  • a C implementation file module.c.

The Blech signature file contains all static information for name checking, type checking and causality analysis. The C header and implementations files are used to compile a Blech program.

Compiling a Blech program

A Blech program file is not a module. Therefore, it cannot be imported. The compilation of a Blech program file, for example program.blc, generates only

  • A C header file program.h, and
  • a C implementation file program.c.

Hierarchical organisation of a Blech project

A Blech project consists of modules and program files, that are hierarchically structured.

Abstract types and the diamond import problem

Last modified May 4, 2021: drafting the module chapter (fa3db01)