Cella Documentation 0.1.0-dev Home Source

Conditional compilation

Some code belongs only in some builds: a Windows call that doesn't exist on Linux, logging that a release build should leave out, a check that only debug builds pay for. Cella decides this at compile time with build flags. A when block keeps its contents only in builds whose flags match, a use when line keeps or drops a whole file, and a function marked when disappears, calls and all, from builds where its flag is off.

when blocks

when is followed by a flag condition and a block. In a build where the condition holds, the block's contents are compiled as if the when weren't there. Otherwise they're left out:

main: fun{	when debug	{		printf("debug build\n")	} 	when release	{		printf("release build\n")	}}
Output
debug build

Built with --release, the same program prints the other line:

main: fun{	when debug	{		printf("debug build\n")	} 	when release	{		printf("release build\n")	}}
Output
release build

The braces are required, even around a single statement. A when block doesn't start a scope, so a declaration inside it stays visible after it, as the next section shows.

else when and else

else when and else chain further branches. The first branch whose condition holds is kept, and else is kept when none does:

main: fun{	when release	{		val mode: cstr = "release"	}	else when debug && overflowChecks	{		val mode: cstr = "checked debug"	}	else	{		val mode: cstr = "debug"	} 	printf("%s\n", mode)}
Output
checked debug

Each branch declares mode, and only one of them is ever compiled, so the printf after the chain sees exactly one mode.

Conditions

A condition is made of flag names, !, &&, || and parentheses, with the same meaning and precedence as in ordinary expressions. Nothing else can appear in it: no values, no comparisons, no calls. A name that isn't a flag is an error, with a suggestion when a flag has a close spelling:

mod example main: fun{	when debgFlag 'debg' not foundDid you mean 'debug'?	{	} 	when profilingFlag 'profiling' not found	{	}}

Where when blocks go

when blocks can surround top-level declarations, members of records, enums, traits and other types, enum cases, the members of an ext block, statements, and the contents of visibility blocks:

when debug{	label: const cstr = "debug"}else{	label: const cstr = "release"} Counter: rec{	n: i32 	when debug	{		describe: fun(self) => printf("%s counter at %d\n", label, self.n)	}	else	{		describe: fun(self) => printf("%d\n", self.n)	}} Step: enum{	start 	when debug	{		traced	} 	finish} main: fun{	val c = Counter(4)	c.describe()	printf("%d\n", i32(Step.finish))}
Output
debug counter at 4
2

Step.finish is 2 in a debug build, because traced takes the value 1. In a release build traced doesn't exist and finish is 1.

Inside a match, when groups arms, with else for the other builds. The match's else arm stays the last arm:

size: fun(x: i32) -> i32 => match x{	0 => 0	when debug	{		1 => 10	}	else	{		1 => 20	}	else => 5} main: fun{	printf("%d %d %d\n", size(0), size(1), size(2))}
Output
0 10 5

Code in other builds is only parsed

A branch that isn't kept is parsed but not checked. It can call functions that don't exist in this build, use types that don't exist and name symbols from other platforms, so platform-specific code compiles on every platform:

when windows{	ext("kernel32")	{		GetCurrentProcessId: fun -> u32	} 	processId: fun -> u32 => GetCurrentProcessId()}else{	ext	{		getpid: fun -> i32	} 	processId: fun -> u32 => u32(getpid())} main: fun -> i32 => i32(processId() % 100)

On Windows, only the first branch is checked and built; on Linux, only the second. Building with --target, as described in Projects and building, checks the branches for another platform.

Syntax errors are reported in every branch, since every branch is parsed:

mod example main: fun{	when linux && windows	{		val c = 'ab'Invalid character literal	}}

Built-in flags

These flags are always defined:

FlagOn when
windows, linux, macosbuilding for that operating system
x64, x86, arm64, wasm64building for that processor
debugbuilding without --release
releasebuilding with --release
boundsChecksthe project's BoundsChecks setting is on
overflowChecksthe project's OverflowChecks setting is on

boundsChecks and overflowChecks follow the settings of the project that declares the code, so code that checks its own bounds or overflow can do it exactly when the built-in checks are on. OverflowChecks is on by default in debug builds only:

main: fun{	when overflowChecks	{		printf("overflow checks on\n")	}	else	{		printf("overflow checks off\n")	}}
Output
overflow checks off

Checks and panics covers both settings.

Project flags

A project declares its own flags in a [Flags] table in its .celp file, each with a default value:

OutputType = "Executable" [Flags]logging = trueprofiling = false
main: fun{	when logging	{		printf("logging\n")	} 	when profiling	{		printf("profiling\n")	}}
Output
logging

The table can also be written on one line, as Flags = { logging = true, profiling = false }. A flag name must be a valid identifier and can't be the name of a built-in flag.

--flag sets a flag for one build, overriding its default. --flag profiling turns it on, and --flag profiling=false turns it off:

cella build --flag profilingcella run --flag logging=false

Built with --flag profiling, the program above prints both lines:

main: fun{	when logging	{		printf("logging\n")	} 	when profiling	{		printf("profiling\n")	}}
OutputType = "Executable" [Flags]logging = trueprofiling = false
Output
logging
profiling

--flag only sets flags the project declares. A name that isn't declared is "Flag 'missing' not found", and setting a built-in flag such as debug is "Cannot set built-in flag 'debug'". Flags belong to the project that declares them: a library's code is built with the flags and defaults in the library's own .celp.

Leaving out whole files

use when with a condition, written right after the mod line, keeps or leaves out the whole file. A file left out is only parsed, like a when branch that isn't kept:

main.ce
main: fun{	printf("%s\n", mode())}
debug.ce
mod exampleuse when debug mode: fun -> cstr => "debug"
release.ce
mod exampleuse when !debug mode: fun -> cstr => "release"
Output
debug

Each build sees exactly one mode. The use when line must directly follow the mod line, before any other import ("'use when' must directly follow the module name"), and a file has at most one:

mod exampleuse when debugCannot declare more than one 'use when'use when windowsCannot declare more than one 'use when' main: fun{}

when functions

A function can carry a condition of its own, written after its signature: trace: fun(value: i32) when logging. Unlike a when block, a when function is checked in every build. In builds where its condition holds, it's an ordinary function. In other builds it doesn't exist, and every call to it is removed along with its arguments, so they aren't evaluated:

calls: var i32 = 0 count: fun(value: i32) -> i32{	calls += 1	ret value} trace: fun(value: i32) when debug{	printf("trace %d\n", value)} measure: fun(value: i32) when release{	printf("measure %d\n", value)} main: fun{	trace(count(1))	measure(count(2))	printf("%d calls\n", calls)}
Output
trace 1
1 calls

The call to measure and its argument count(2) are gone from this debug build, so count ran once.

Because the arguments are still checked as if the call ran, a program means the same in every build. Here log would use text after it was moved, which is an error even in a release build where log is never called:

Text: own rec{	id: i32 	drop: op	{	}} take: fun(text: Text){} log: fun(imm text: Text) when release{} main: fun{	val text = Text(1)	take(text)	log(imm text)Cannot use moved valuesMoved on line 23}

To leave code out unchecked, put it in a when block instead.

Methods and static functions can be when functions too, and generic functions and functions taking templates work the same way. Since calls to them may vanish, when functions are limited to what a removed call can't miss:

  • They return nothing, and they can't be -> never.
  • Their parameters without a mode must have types that copy, since a removed call would leave a moved value unmoved.
  • They can't be used as values.
  • main, constructors, destructors, getters, setters, operators and ext functions can't be when functions, a trait can't require one, and one can't implement a trait's member.
total: fun(value: i32) -> i32 when debug => valueCannot return values from 'when' functions keep: fun(file: File) when debugCannot copy 'File'{} trace: fun(value: i32) when debug{} apply: fun(imm action: fun[i32]) => action(1) main: fun{	apply(trace)Cannot use 'when' functions as values}

Conditional project settings

A .celp file can hold tables named [When."condition"], with a condition written as in when. Their settings apply only to builds where the condition holds and override the settings at the top of the file. A condition that is a single flag can drop the quotes, as in [When.debug]:

OutputType = "Executable"OverflowChecks = true [Flags]fast = false [When."release && fast"]OverflowChecks = false
main: fun{	when overflowChecks	{		printf("checked\n")	}	else	{		printf("unchecked\n")	}}
Output
checked

Built with --release --flag fast, the same program prints unchecked.

A conditional table can also add project references, which are added to the ones at the top level:

[[When."windows || linux".ProjectReferences]]Path = 'platform/platform.celp'

OutputType and [Flags] can't be set in a conditional table. When two tables that both apply set the same setting, the build fails with an error naming both tables. Project settings lists which settings can be conditional.