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:
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)
endA 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.
| Flag | True when building for |
|---|---|
SALAM_OS_WINDOWS | Windows, together with SALAM_OS_WIN64 or SALAM_OS_WIN32 |
SALAM_OS_LINUX | Linux and Android |
SALAM_OS_MAC | macOS |
SALAM_OS_ANDROID | Android |
SALAM_OS_FREEBSD, SALAM_OS_OPENBSD, SALAM_OS_NETBSD | each BSD |
SALAM_OS_BSD | macOS and the BSDs |
SALAM_OS_UNIX | every Unix-like system, WebAssembly included |
SALAM_OS_WASM | WebAssembly |
SALAM_ARCH_X64, SALAM_ARCH_X86 | 64-bit and 32-bit Intel and AMD |
SALAM_ARCH_ARM64, SALAM_ARCH_ARM | 64-bit and 32-bit ARM |
SALAM_ARCH_WASM | WebAssembly |
SALAM_SAFE | any 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:
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
endAlways 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:
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
endWhat 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:
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
endThat 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:
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)
endBinary 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-gnuchecks the Windows branchessalam obj app.salam --target=aarch64-apple-darwinchecks 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
ifanelse, 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
--targetfor 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.