Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Build options

Some code should be optional at build time: linking a library such as libcrypto or zlib when it is there, tracing that only a debug build wants, a backend chosen when the program is built. Build options set that on the command line with -D, as in C compilers, and the code tests them with BuildOption.

pep-build main.pep -D USE_LIBCRYPTO -D LOG_LEVEL=2 -D BACKEND=zlib -o app
  • -D NAME defines an option. It counts as NAME=1, as in C.
  • -D NAME=VALUE defines it with a value, which may be any text.
  • Repeat -D for more options. When two name the same option, the later one wins.

pep-build --test defines TEST, which keeps the [Test] functions and can mark code only tests use (see Unit tests).

A name is a letter or _, then letters, digits and _. Any other name is an error:

error: invalid build option name '2FAST' in '-D 2FAST'; a name is a letter or '_' followed by letters, digits and '_'
# Build options: compile with `pep-build main.pep -D FAST_MATH -D LOG_LEVEL=2`
# to pick the other versions.

# `[BuildOption("NAME")]` keeps a declaration when `-D NAME` is given, and
# `!BuildOption(NAME)` inside a `Target` keeps its fallback otherwise.
[BuildOption("FAST_MATH")]
scale(x: int): int {
    return x << 1;
}

[Target(!BuildOption(FAST_MATH))]
scale(x: int): int {
    return x * 2;
}

# A `static` block reads an option's value while compiling, with a default
# for a build that does not set it.
log_level(): int {
    var level = 1;
    static (level) {
        if (reflect.build_option("LOG_LEVEL", "1") == "2") {
            level = 2;
        }
    }
    return level;
}

main(): int {
    if (scale(21) != 42) {
        return 1;
    }
    var level = log_level();
    if (level < 1 || level > 2) {
        return 2;
    }
    return 0;
}

Selecting code

[BuildOption("NAME")] keeps a declaration or a block only when the build defines NAME. It is shorthand for [Target(BuildOption(NAME))], and it goes everywhere [Target(...)] goes: functions, externs, structs, methods, module-scope variables, imports, re-exports and { } blocks in a function (see Platform selection).

Inside Target, BuildOption(NAME) combines with OS(...), Arch(...), !, &&, || and parentheses. Used alone, it is true when NAME is defined, whatever its value, and false when it is not, with no warning:

[BuildOption("USE_LIBCRYPTO")]
[Link("crypto")]
extern int SHA256_Init(__ptr<void> context);

[Target(!BuildOption(USE_LIBCRYPTO))]
_sha256_compress(...) { ... }   # the portable fallback

[Target(BuildOption(TRACE) && OS(Linux))]
import "linux_trace";

Several attributes on one item combine with “and”, as [Target] does.

Options the runtime reads

OptionEffect
USE_LIBCRYPTOdigest’s SHA-256 and SHA-1 use libcrypto (CNG on Windows); see Cryptographic hashes
USE_ZLIBcompress uses zlib; see Compression
TESTdefined by pep-build --test; see Unit tests

Comparing values

BuildOption(NAME) may be compared with a literal using ==, !=, <, <=, > and >=, on either side. The literal’s type says how the value is read:

LiteralThe value is read asExample
integera signed 64-bit integer, decimal or 0x hexBuildOption(LOG_LEVEL) >= 2
floata doubleBuildOption(SCALE) > 1.5
true / falsetrue/false, 1/0, on/off or yes/no, in any case (== and != only)BuildOption(TRACE) == true
stringtext, compared byte for byteBuildOption(BACKEND) == "zlib"
[Target(BuildOption(LOG_LEVEL) >= 2 && OS(Linux))]
_trace(message: String) { ... }

[Target(BuildOption(BACKEND) == "zlib")]
import "zlib_backend";

A value the literal’s type cannot read is an error at the comparison:

main.pep:1:28: error: build option 'LEVEL' is 'high', which is not an integer
    1 | [Target(BuildOption(LEVEL) >= 2)]
      |                            ^~~~

Comparing two build options, or a build option with anything but a literal, is an error too.

A comparison with an option the build does not define is false, whatever the operator, and the compiler warns at it, since a missing option there is usually a mistake:

main.pep:3:13: warning: build option 'LOG_LEVEL' is not defined; the comparison is false
    3 |     [Target(BuildOption(LOG_LEVEL) >= 2)] {
      |             ^~~~~~~~~~~~~~~~~~~~~~
note: define it with '-D LOG_LEVEL=<value>'

Reading values in static blocks

A static block reads build options while compiling:

  • reflect.has_build_option("NAME") is whether the build defines NAME.
  • reflect.build_option("NAME") is its value, as a String. It is an error when the build does not define NAME.
  • reflect.build_option("NAME", "default") is its value, or default when the build does not define it.

The build cache and the editor

The options and their values are part of the build cache’s key, so building with other options never reuses objects built with different ones.

The language server analyzes with the options in the VS Code setting pepper.buildOptions, a list written as -D takes them (["USE_ZLIB", "LOG_LEVEL=2"]). It has none by default, so code that needs an option shows dimmed, and comparisons with options warn until the setting names them.

Not yet

  • A pep.toml table declaring the options a package takes, with their types and defaults.
  • Arithmetic on options, and comparing two options.