13. Compilation Units, Packages, and Modules¶
Programs are structured as sequences of elements ready for compilation, i.e., compilation units. Each compilation unit creates its own scope (see Scopes). The compilation unit’s variables, functions, classes, interfaces, or other declarations are only accessible within such scope if not explicitly exported.
A variable, function, class, interface, or other declarations exported from a different compilation unit must be imported first.
Compilation units are separate modules or packages. Packages are described in the chapter Experimental Features (see Packages).
compilationUnit:
separateModuleDeclaration
| packageDeclaration
;
packageDeclaration:
packageModule+
;
All modules are stored in a file system or a database (see Compilation Units in Host System).
13.1. Separate Modules¶
A separate module is a module without a package header. It optionally consists of the following four parts:
Import directives that enable referring imported declarations in a module;
Top-level declarations;
Top-level statements; and
Re-export directives.
separateModuleDeclaration:
importDirective* (topDeclaration | topLevelStatements | exportDirective)*
;
Every module automatically imports all exported entities from essential kernel packages of the standard library (see Standard Library).
All entities from these packages are accessible as simple names, like the console variable:
1 // Hello, world! module
2 function main() {
3 console.log("Hello, world!")
4 }
13.2. Compilation Units in Host System¶
Modules and packages are created and stored in a manner that is determined by a host system.
The exact way modules and packages are stored in a file system is determined by a particular implementation of the compiler and other tools.
In a simple implementation:
A module (package module) is stored in a single file.
Files corresponding to a package module are stored in a single folder.
A folder can store several separate modules (one source file to contain a separate module or a package module).
A folder that stores a single package must contain neither separate module files nor package modules from other packages.
13.3. Import Directives¶
Import directives import entities exported from other compilation units, and provide such entities with bindings in the current module.
An import declaration has the following two parts:
Import path that determines a compilation unit to import from;
Import binding that defines what entities, and in what form—qualified or unqualified—can be used by the current module.
importDirective:
'import' fileBinding|selectiveBindigns|defaultBinding|typeBinding
'from' importPath
;
fileBinding:
'*' importAlias
| qualifiedName '.' '*'
;
selectiveBindigns:
'{' importBinding (',' importBinding)* '}'
;
defaultBinding:
Identifier
;
typeBinding:
'type' selectiveBindigns
;
importBinding:
qualifiedName importAlias?
;
importAlias:
'as' Identifier
;
importPath:
StringLiteral
;
Each binding adds a declaration or declarations to the scope of a module or a package (see Scopes). Any declaration added so must be distinguishable in the declaration scope (see Distinguishable Declarations). Otherwise, a compile-time error occurs.
Some import constructions are specific for packages. They are described in the chapter Experimental Features (see Packages).
13.3.1. Bind All with Qualified Access¶
The import binding ‘* as A’ binds the single named entity ‘A’ to the declaration scope of the current module.
A qualified name, which consists of ‘A’ and the name of entity ‘A.name’, is used to access any entity exported from the compilation unit as defined by the import path.
Import |
Usage |
|
---|---|---|
import * as Math from "..."
|
let x = Math.sin(1.0)
|
This form of import is recommended because it simplifies the reading and understanding of the source code.
13.3.2. Simple Name Binding¶
The import binding ‘qualifiedName’ has two cases as follows:
A simple name (like foo); or
A name containing several identifiers (like A.foo).
The import binding ‘ident’ binds an exported entity with the name ‘ident’ to the declaration scope of the current module. The name ‘ident’ can only correspond to several entities, where ‘ident’ denotes several overloaded functions (see Function and Method Overloading).
The import binding ‘ident as A’ binds an exported entity (entities) with the name ‘A’ to the declaration scope of the current module.
The bound entity is not accessible as ‘ident’ because this binding does not bind ‘ident’.
This is shown in the following module:
1 export const PI = 3.14
2 export function sin(d: number): number {}
The module’s import path is now irrelevant:
Import |
Usage |
|
---|---|---|
import {sin} from "..."
|
let x = sin(1.0)
let f: float = 1.0
|
|
import {sin as Sine} from "
..."
|
let x = Sine(1.0) // OK
let y = sin(1.0) /* Error ‘y’ is
not accessible */
|
A single import statement can list several names:
Import |
Usage |
|
---|---|---|
import {sin, PI} from "..."
|
let x = sin(PI)
|
|
import {sin as Sine, PI} from "
..."
|
let x = Sine(PI)
|
Complex cases with several bindings mixed on one import path are discussed below in Several Bindings for One Import Path.
13.3.3. Several Bindings for One Import Path¶
The same bound entities can use several import bindings. The same bound entities can use one import directive, or several import directives with the same import path.
In one import directive |
import {sin, cos} from "..."
|
In several import directives |
import {sin} from "..."
import {cos} from "..."
|
No conflict occurs in the above example, because the import bindings define disjoint sets of names.
The order of import bindings in an import declaration has no influence on the outcome of the import.
The rules below prescribe what names must be used to add bound entities to the declaration scope of the current module if multiple bindings are applied to a single name:
Case |
Sample |
Rule |
---|---|---|
A name is explicitly used without an alias in several bindings. |
import {sin, sin}
from "..."
|
Ok. The compile-time warning is recommended. |
A name is used explicitly without alias in one binding. |
import {sin}
from "..."
|
Ok. No warning. |
A name is explicitly used without alias and implicitly with alias. |
import {sin}
from "..."
import * as M
from "..."
|
Ok. Both the name and qualified name can be used: sin and M.sin are accessible. |
A name is explicitly used with alias. |
import {sin as Sine}
from "..."
|
Ok. Only alias is accessible for the name, but not the original one:
|
A name is explicitly used with alias and implicitly with alias. |
import {sin as Sine}
from "..."
import * as M
from "..."
|
|
A name is explicitly used with alias several times. |
import {sin as Sine,
sin as SIN}
from "..."
|
Compile-time error. Or warning? |
13.3.4. Default Import Binding¶
Default import binding allows importing a declaration exported from some module as default export. Knowing the actual name of the declaration is not required as the new name is given at importing. A compile-time error occurs if another form of import is used to import an entity initially exported as default.
1 import DefaultExportedItemBindedName from ".../someFile"
2 function foo () {
3 let v = new DefaultExportedItemBindedName()
4 // instance of class 'SomeClass' be created here
5 }
6
7 // SomeFile
8 export default class SomeClass {}
13.3.5. Type Binding¶
Type import binding allows importing only the type declarations exported from some module or package. These declarations can be exported normally, or by using the export type form. The difference between import and import type is that the first form imports all top-level declarations which were exported, and the second imports only exported types.
1 // File module.ets
2 console.log ("Module initialization code")
3
4 export class Class1 {/*body*/}
5
6 class Class2 {}
7 export type {Class2}
8
9 // MainProgram.ets
10
11 import {Class1} from "./module.ets"
12 import type {Class2} from "./module.ets"
13
14 let c1 = new Class1() // OK
15 let c2 = new Class2() // OK, the same
13.3.6. Import Path¶
Import path is a string literal—represented as a combination of the slash character ‘/’ and a sequence alpha-numeric characters—that determines how an imported compilation unit must be placed.
The slash character ‘/’ is used in import paths irrespective of the host system. The backslash character is not used in this context.
In most file systems, an import path looks like a file path. Relative (see below) and non-relative import paths have different resolutions that map the import path to a file path of the host system.
The compiler uses the following rule to define the kind of imported compilation units, and the exact placement of the source code:
If import path refers to a folder denoted by the last name in the resolved file path, then the compiler imports the package that resides in the folder. The source code of the package is all the ArkTS source files in the folder.
Otherwise, the compiler imports the module that the import path refers to. The source code of the module is the file with the extension provided within the import path, or—if none is so provided—appended by the compiler.
A relative import path starts with ‘./’ or ‘../’ as in the following examples:
1 "./components/entry"
2 "../constants/http"
Resolving a relative import is relative to the importing file. Relative import is used on compilation units to maintain their relative location.
1 import * as Utils from "./mytreeutils"
Other import paths are non-relative as in the examples below:
1 "/net/http"
2 "std/components/treemap"
Resolving a non-relative path depends on the compilation environment. The definition of the compiler environment can be particularly provided in a configuration file or environment variables.
The base URL setting is used to resolve a path that starts with ‘/’. Path mapping is used in all other cases. Resolution details depend on the implementation.
For example, the compilation configuration file can contain the following lines:
1 "baseUrl": "/home/project",
2 "paths": {
3 "std": "/arkts/stdlib"
4 }
In the example above, ‘/net/http’ is resolved to ‘/home/project/net/http’, and ‘std/components/treemap’ to ‘/arkts/stdlib/components/treemap’.
File name, placement, and format are implementation-specific.
13.4. Default Import¶
Any compilation unit automatically imports all entities exported from the essential kernel packages of the standard library(see Standard Library). All entities from this package can be accessed as simple names.
1 function main() {
2
3 let myException = new Exception { ... }
4 // class 'Exception' is defined in the standard library
5
6 console.log("Hello")
7 // variable 'console' is defined in the standard library too
8
9 }
13.5. Top-Level Declarations¶
Top-level declarations declare top-level types (class, interface, or enum), top-level variables, constants, or functions. Top-level declarations can be exported.
topDeclaration:
('export' 'default'?)?
( typeDeclaration
| typeAlias
| variableDeclarations
| constantDeclarations
| functionDeclaration
| extensionFunctionDeclaration
)
;
1 export let x: number[], y: number
13.5.1. Exported Declarations¶
Top-level declarations can use export modifiers that make the declarations accessible in other compilation units by using import (see Import Directives). The declarations not marked as exported can be used only inside the compilation unit they are declared in.
1 export class Point {}
2 export let Origin = new Point(0, 0)
3 export function Distance(p1: Point, p2: Point): number {
4 // ...
5 }
In addition, only one top-level declaration can be exported by using the default export scheme. It allows specifying no declared name when importing (see Default Import Binding for details). A compile-time error occurs if more than one top-level declaration is marked as default.
1 export default let PI = 3.141592653589
13.6. Export Directives¶
The export directive allows the following:
Specifying a selective list of exported declarations with optional renaming; or
Re-exporting declarations from other compilation units.
exportDirective:
selectiveExportDirective | reExportDirective | exportTypeDirective
;
13.6.1. Selective Export Directive¶
In addition, each exported declaration can be marked as exported by explicitly listing the names of exported declarations. Renaming is optional.
selectiveExportDirective:
'export' selectiveBindigns
;
An export list directive uses the same syntax as an import directive with selective bindings:
1 export { d1, d2 as d3}
The above directive exports ‘d1’ by its name, and ‘d2’ as ‘d3’. The name ‘d2’ is not accessible in the modules that import this module.
13.6.2. Export Type Directive¶
In addition to export that is attached to some declaration, a programmer can use the export type directive in order to do the following:
Export as a type a particular class or interface already declared; or
Export an already declared type under a different name.
The appropriate syntax is presented below:
exportTypeDirective:
'export' 'type' selectiveBindigns
;
If a class or an interface is exported in this manner, then its usage is limited similarly to the limitations described for import type directives (see Type Binding).
A compile-time error occurs if a class or interface is declared exported, but then export type is applied to the same class or interface name.
The following example is an illustration of how this can be used:
1 class A {}
2
3 export type {A} // export already declared class type
4
5 export type MyA = A // name MyA is declared and exported
6
7 export type {MyA} // compile-time error as MyA was already exported
13.6.3. Re-Export Directive¶
In addition to exporting what is declared in the module, it is possible to re-export declarations that are part of other modules’ export. Only limited re-export possibilities are currently supported.
It is possible to re-export a particular declaration or all declarations from a module. When re-exporting, new names can be given. This action is similar to importing but with the opposite direction.
The appropriate grammar is presented below:
reExportDirective:
'export' ('*' | selectiveBindigns) 'from' importPath
;
The following examples illustrate the re-exporting in practice:
1 export * from "path_to_the_module" // re-export all exported declarations
2 export { d1, d2 as d3} from "path_to_the_module"
3 // re-export particular declarations some under new name
13.7. Top-Level Statements¶
A separate module can contain sequences of statements that logically comprise one sequence of statements:
topLevelStatements:
statements
;
A module can contain any number of top-level statements that logically merge into a single sequence in the textual order:
1 statements_1
2 /* top-declarations */
3 statements_2
The sequence above is equal to the following:
1 /* top-declarations */
2 statements_1; statements_2
If a separate module is imported by some other module, then the semantics of top-level statements is to initialize the imported module. It means that all top-level statements are executed only once before a call to any other function, or before the access to any top-level variable of the separate module.
If a separate module is used a program, then top-level statements are used as a program entry point (see Program Entry Point (main)). If the separate module has the
main
function, then it is executed after the execution of the top-level statements.
1 // Source file A
2 { // Block form
3 console.log ("A.top-level statements")
4 }
5
6 // Source file B
7 import * as A from "Source file A "
8 function main () {
9 console.log ("B.main")
10 }
The output is as follows:
Top-level statements,
Main.
1 // One source file
2 console.log ("A.Top-level statements")
3 function main () {
4 console.log ("B.main")
5 }
A compile-time error occurs if a top-level statement contains a return statement (Expression Statements).
13.8. Program Entry Point (main)¶
Separate modules can act as programs (applications). The two kinds of program (application) entry points are as follows:
Top-level statements (see Top-Level Statements);
Top-level
main
function (see below).
Thus, a separate module may have:
Only a top-level
main
function (that is the entry point);Only top-level statements (that are entry points);
Both top-level
main
function and statements (same as above, plusmain
is called after the top-level statement execution is completed).
The top-level main
function must have either no parameters, or one
parameter of string type []
that provides access to the arguments of
program command-line. Its return type is either void
or int
.
No overloading is allowed for an entry point function.
Different forms of valid and invalid entry points are shown in the example below:
1 function main() {
2 // Option 1: a return type is inferred, must be void or int
3 }
4
5 function main(): void {
6 // Option 2: explicit :void - no return in the function body required
7 }
8
9 function main(): int {
10 // Option 3: explicit :int - return is required
11 return 0
12 }
13
14 function main(): string { // compile-time error: incorrect main signature
15 return ""
16 }
17
18 function main(p: number) { // compile-time error: incorrect main signature
19 }
20
21 // Option 4: top-level statement is the entry point
22 console.log ("Hello, world!")