< back to index

Program structure

A Millfork program is build from one or more modules.

Each module is stored in a single file. All source filenames passed to the compiler are considered to be modules of that program, called root modules.

Each module has a name, which is its unique identifier. A module name is a sequence of slash-separated valid Millfork identifiers. The name also defines where the module is located: a module named a/b is presumed to exist in a/b.mfk and it's looked up first in the directory that contains the current source file, then in the current working directory, and then in the include directories.

A module can import other modules, using the import statement. Importing the same module multiple times merely marks it as imported by multiple modules, but the program will still contain only one copy of it. Examples:

import string
import cbm_file

Usually, the imported module will undergo the first phase of compilation first. This means that the constants in the imported module will be resolved first, allowing you to use them in the importing module.

The only exception to this rule is when the importing graph has a cycle, in which case the order of modules within the cycle is unspecified.

A platform may define starting modules using the modules= directive of the [compilation] section. All starting modules are considered to be imported by all source files explicitly mentioned on the command line.

Module templates

If the first line of a source file starts with the #template directive, then the source is considered to be a module template. Module templates are a tool for generating repetitive code, similar to COBOL copybooks or Go Generate.

The template directive contains a comma-separated list of parameters. It's recommended that the names of parameters begin and end with non-alphanumeric characters:

#template $P1$, $P2$

A module template cannot be imported as-is. When importing a module template, you import a concrete instantiation of it.

For example, if the file temp.mfk contains the #template from above, you can import it by providing a list of numeric literals or identifiers:

import temp<1, 2>

This instantiates a new module named temp<1,2> (if it hasn't been instantiated anywhere else). The code in that module is generated by replacing every instance of the parameter names with the actual argument content. Parameters that are numeric literals are normalized to their decimal representations.

Your program may contain multiple modules created from the same template with different parameters. For example,

import temp<3, 4>
import temp<5, 6>
import temp<$5, $6>

instantiates and imports two similar, yet different modules: temp<3,4> and temp<5,6>. The third import imports a module that has already been instantiated and imported, so it's redundant.

The instantiation works through simple text replacement. For example, if temp.mfk contains:

#template $P1$, $P2$
const byte a$P1$ = $P2$

then the temp<1,2> module will contain

const byte a1 = 2

This substitution is performed before preprocessing, so those substitutions are available for the preprocessor directives. It applies to identifiers, string literals, keywords, preprocesor directives etc.

Warning: This mechanism provides no direct way for preventing duplicates of code that does not depend on the template parameters, or depends on only some template parameters. In such situations, it might be advisable to put the non-dependent definitions in another module that is not a template, or in a module template with fewer parameters. For example, instead of writing:

#template $N$
const byte X = 50
array a$N$ [X]

(which would define duplicate Xs if imported multiple times), consider writing two files:

#template $N$
import define_X
array a$N$ [X]
//define_X.mfk:
const byte X = 50