Skip to content

Modules

A program consists of 1-or-more modules which define a collection of imports, functions and global variables. Modules may be combined into a single program and compiled together.

Module          = "module" Identifier '{' ModuleContents '}' ;
ModuleContents  = { ImportDirective | FunctionDeclaration | GlobalDeclaration } ;

Built-in Modules

HorseIR provides a built-in module "Builtin" that implements the basic mathematical and database operations. It exists as a pseudo-module and its implementation is provided by the compiler. As there are no operators (see Operators), this forms the core of the language functionality. Defining the built-in set as a regular module provides shadowing behaviour consistent with user-defined code.

HorseIR provides a second built-in module "System" which defines system variables and their respective default values. For example, pp controls the precision of output, with default value 10. Importing the system module allows programs to customize their local system environment.

Import Directives

Modules may be composed into larger programs, either by importing all module contents (.*), a specific element (.Identifier), or a list of elements (.{Identifier, ...}). Imports may include functions or global variables, however, they are not transitive.

ImportDirective  = "import" Identifier '.' ImportList ';' ;
ImportList       = '*' | Identifier | '{' Identifier { ',' Identifier } '}' ;

Function Declarations

Functions define a collection of statements with 0-or-more input parameters and 0-or-more return types (supporting multiple returns). Each input parameter defines its name and type.

FunctionDeclaration  = FunctionKind Identifier '(' Parameters ')'
                           [ ':' ReturnTypes ] Block ;

FunctionKind  = "def" | "kernel" ;

Parameters    = [ Parameter { ',' Parameter } ] ;
Parameter     = Identifier ':' Type ;

ReturnTypes   = Type { ',' Type } ;

If the function specifies a return type, then the body must return on all paths.

The function kind specifies its intended execution target. def indicates a generic function while kernel directs the runtime system to use connected GPUs.

Entry Function

Execution begins with an entry function main with optional input parameter args:List<?> and any return type. When invoking a program, the entry module to search must be specified.

Global Declarations

Global variables belong to modules, and may be shared through the import directive. Each global variable consists of a name and associated type.

GlobalDeclaration  = "global" Identifier ':' Type '=' Operand ';' ;

Scope Rules

There are following scopes in a program:

  1. Program scope: All modules in the compilation unit.
  2. Module scopes: Functions and global variables in a module. Contents may be declared in any order.
  3. Function scopes: Parameters and local variables in a function. Variables must be declared before use.
  4. Block scopes: Blocks defined as part of control-flow structures define new scopes.

While there may be multiple module and function scopes, there is only a single program scope.

Name Uniqueness

Declarations within a scope must be unique:

  • A module name in the program scope
  • A method name in a module
  • A global variable in a module

Name Resolution

To resolve the use of an identifier, the compiler checks:

  1. Block scopes (if any)
  2. Function scope
  3. Module scope
  4. Imported content

Local variables shadow global declarations, and global declarations shadow imported content.

Imported module content may optionally be used without the module name (i.e. as sum instead of Bultin.sum) if they have not been shadowed. In the case of shadowing, the fully qualified name is required. Both global variables and functions follow the same rules. Local variables cannot be imported.

Example

module A {
    def x() : i32 { ... }
    def y() : i32 { ... }
}

module main {
    import A.*;

    def x() : i32 { ... }

    def main() {
    a:i32 = @x();    // Resolves to main.x
    b:i32 = @y();    // Resolves to A.y

    c:i32 = @A.x();  // Resolves to A.x
    }
}

When two imported modules contain an element of the same name, the last import shadows the earlier import.