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:
- Program scope: All modules in the compilation unit.
- Module scopes: Functions and global variables in a module. Contents may be declared in any order.
- Function scopes: Parameters and local variables in a function. Variables must be declared before use.
- 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:
- Block scopes (if any)
- Function scope
- Module scope
- 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.