Lesson 21 of 25 · Larger programs

Conditional compilation

Sometimes code has to differ a little between Windows, macOS and Linux, or between a debug build and a release build. Salam makes that choice while compiling, so the branch that does not apply is not in the program at all and costs nothing at run time.

if at compile time

An if at the top level of a file, with a condition known while compiling, keeps only the branch that applies. Whatever is declared inside it, such as constants, globals, extern blocks or functions, exists only on the platforms where that branch is taken. Here each platform gets its own path separator, and the rest of the program uses SEP without caring where it runs:

comptime.salam
import str

if SALAM_OS_WINDOWS:
    const SEP := "\\"
else:
    const SEP := "/"
end

func main:
    path := "home" + SEP + "sara" + SEP + "notes.txt"
    parts := str.Split(path, SEP)
    defer parts.free()
    println parts.len(), "parts"
    println parts.get(2)
end
Output3 parts notes.txt

A top-level if needs a condition the compiler can work out on its own. Writing if count > 3: at the top level is the error "top-level if condition must be a compile-time constant".

The flags you get

The compiler defines a set of bool flags that describe the machine the program is built for. One operating system flag and one processor flag are true, plus the family flags that go with them: Linux and macOS also set SALAM_OS_UNIX, Android sets SALAM_OS_LINUX as well, and macOS and the BSDs set SALAM_OS_BSD.

FlagTrue when building for
SALAM_OS_WINDOWSWindows, together with SALAM_OS_WIN64 or SALAM_OS_WIN32
SALAM_OS_LINUXLinux and Android
SALAM_OS_MACmacOS
SALAM_OS_ANDROIDAndroid
SALAM_OS_FREEBSD, SALAM_OS_OPENBSD, SALAM_OS_NETBSDeach BSD
SALAM_OS_BSDmacOS and the BSDs
SALAM_OS_UNIXevery Unix-like system, WebAssembly included
SALAM_OS_WASMWebAssembly
SALAM_ARCH_X64, SALAM_ARCH_X8664-bit and 32-bit Intel and AMD
SALAM_ARCH_ARM64, SALAM_ARCH_ARM64-bit and 32-bit ARM
SALAM_ARCH_WASMWebAssembly
SALAM_SAFEany build made without --release

A few more hold a value instead of true or false. SALAM_OS is text such as "windows", "linux", "mac", "android" or "freebsd", and SALAM_ARCH is "x64", "x86", "arm64", "arm" or "wasm". SALAM_VERSION is the compiler version as text, such as "0.5.0", and SALAM_VERSION_CODE the same version as a number, 500. In a Persian program each of these names has a Persian spelling; the glossary lists them all.

Inside a function

The same if works inside a function body. When its condition is made of these flags, the compiler keeps the branch that applies and throws the others away, exactly as at the top level. An else with a condition chains the platforms:

comptime-inline.salam
func config_dir(app: str): str:
    if SALAM_OS_WINDOWS:
        ret "C:\\Users\\Public\\AppData\\" + app
    else SALAM_OS_MAC:
        ret "/Users/Shared/Library/Application Support/" + app
    else:
        ret "/etc/" + app
    end
end

func main:
    println config_dir("salam")
    if SALAM_OS_UNIX:
        println "permissions: chmod 600"
    end
end

Always give the platform branches an else. A function that does if SALAM_OS_WINDOWS: ret ... end and then carries on with the Unix code builds fine on Linux, but on Windows that if becomes "always return", and the code after it is error E070, unreachable code. With an else both builds are clean.

The branch that is kept is not a block of its own: what it declares stays visible after the if. You can write user := "USERNAME" in the Windows branch and user := "USER" in the other one, and use user below them.

Flags as ordinary values

Outside an if these names are plain constants. You can print one, store it in a variable, use it in a switch, or mix it with a test that runs at run time, as in if SALAM_OS_WINDOWS and retries > 0:, where only the second half is left for the running program:

comptime-value.salam
func family(): str:
    switch SALAM_OS:
        "linux", "android":
            ret "linux"
        end
        "mac", "freebsd", "openbsd", "netbsd":
            ret "bsd"
        end
        "windows":
            ret "windows"
        end
        else:
            ret "other"
        end
    end
end

func main:
    println "built for", SALAM_OS, "on", SALAM_ARCH
    println "family:", family()
    wide := SALAM_ARCH_X64 or SALAM_ARCH_ARM64
    println "64-bit:", wide
    println "salam", SALAM_VERSION
end

What this program prints depends on the machine it is built for, so it is shown without its output. On a 64-bit Linux PC it prints built for linux on x64, then family: linux, 64-bit: true and the version.

Your own flags with -D

You can add flags of your own from the command line. -DNAME defines a flag that is true, and -DNAME=value one that holds text. defined(NAME) asks whether a flag was given at all:

comptime-define.salam
if defined(MODE) and MODE == "fast":
    const SAMPLES := 10
else:
    const SAMPLES := 1000
end

func main:
    println "samples:", SAMPLES
    if defined(VERBOSE):
        println "verbose output is on"
    else:
        println "pass -DVERBOSE for more detail"
    end
end
Outputsamples: 1000 pass -DVERBOSE for more detail

That output comes from a plain salam run. With salam run app.salam -DMODE=fast -DVERBOSE the same program prints samples: 10 and verbose output is on. This is the usual way to keep a feature, a test hook or extra logging out of the normal build.

Two details matter. At the top level a flag nobody defined counts as false, but inside a function an unknown name is still unknown: if VERBOSE: without -DVERBOSE is error E001. And comparing a missing flag with text, MODE == "fast", is not a compile-time condition at all. Asking defined(...) first, as above, works in both places.

Constants from the command line

A flag decides which code is compiled. When all you want is a value, such as a port number or a build label, use -d instead: salam build app.salam -dPORT=9090 -dLABEL=beta gives the program the constants PORT and LABEL, as if the file said const PORT := 9090. The value is read as a number, or as true or false, when it looks like one, and as text otherwise; a bare -dDEBUG means true. The name must be written in capitals, and a constant the file declares itself wins over one given with -d.

Debug-only checks

SALAM_SAFE is true in every build made without --release. That makes it the place for checks you want while developing and do not want to pay for in the program you ship:

comptime-safe.salam
if SALAM_SAFE:
    const BUILD := "debug"
else:
    const BUILD := "release"
end

func find(xs: int[], want: int): int:
    if SALAM_SAFE:
        repeat len(xs) - 1 in i:
            if xs[i] > xs[i + 1]:
                printerrln "find: the input is not sorted"
                break
            end
        end
    end
    mut lo := 0
    mut hi := len(xs) - 1
    until lo <= hi:
        mid := (lo + hi) / 2
        if xs[mid] == want:
            ret mid
        else xs[mid] < want:
            lo = mid + 1
        else:
            hi = mid - 1
        end
    end
    ret -1
end

func main:
    println "build:", BUILD
    println find([2, 5, 8, 13, 21], 13)
    println find([9, 4, 7, 1], 9)
end
Outputbuild: debug 3 find: the input is not sorted -1

Binary search only works on sorted input. In the debug build the extra loop notices the unsorted array and says so, which explains why the second search misses a number that is there. Built with --release, the loop is not compiled at all and the program prints only the two results.

Checking the other platforms

Because the branches that do not apply are thrown away before type checking, a mistake inside them stays hidden on your machine: a call to a function that does not exist, written in the Windows branch, compiles without a word on Linux. To check another platform without owning one, compile for it with --target:

  • salam obj app.salam --target=x86_64-windows-gnu checks the Windows branches
  • salam obj app.salam --target=aarch64-apple-darwin checks the macOS branches

obj stops once it has an object file, so it needs no linker or system libraries for that target, and every error it reports is a real one.

Rules of thumb

  • Keep platform differences in one small place, such as a constant or a short function, and the rest of the program the same everywhere.
  • Give every platform if an else, especially when a branch returns.
  • Test your own flags with defined(NAME), so a build without them still compiles.
  • Before you share platform code, build it once with --target for each system you support.

To call a different C library on each system, put the extern declarations themselves inside a top-level if. The next lesson, Working with C, starts from there.