Constants and global variables
A constant is declared at the top level of a file or inside a function, and must always be initialized:
const int MaxPlayers = 8;const double Pi = 3.14159;const string Greeting = "Hello, " + "World!"; // string concatenation is allowedconst Color Favorite = Color.Green; // enum members tooconst int Doubled = MaxPlayers * 2; // other constants, operators, castsOnly numbers, bool, char, string and enums are allowed as a constant’s type. A local constant (inside a
function) has no storage — its value is inlined wherever it’s used, and assigning to it is an error. A top-level
constant may be used before its own declaration in the file, and is checked even if nothing ever uses it.
The compiler evaluates every constant at compile time, following the exact same rules the generated code would use at run time (literal types, the target type or promotion of small integers (see basics), checked arithmetic, shifts, comparisons, casts that saturate floating-point values, string concatenation). The difference is that overflow, division by zero, and similar problems are reported as compile errors, not runtime panics:
const int Broken = 2147483647 + 1; // compile error: integer overflow in a constant expressionThe same rules apply to enum member values (B = A * 2, C = sizeof(int64) are both fine).
A constant always names its type: const var is an error.
Constant slices
Section titled “Constant slices”A constant can also hold several values, as a ReadOnlySlice<T> of numbers, bool, char, string or enums,
written as a collection expression that may spread other
constant slices:
const ReadOnlySlice<int> Primes = [2, 3, 5, 7];const ReadOnlySlice<int> More = [..Primes, 11, 13];const ReadOnlySlice<string> Names = ["Red", "Green", "Blue"];
const int Largest = More[^1]; // indexing, ^n, slicing and Length work in constants tooconst int Count = Primes[1..].Length; // 3const int Letters = "hello".Length; // (also for constant strings)- Arrays and
Slice<T>cannot be constants, because their elements can be changed; the error suggestsReadOnlySlice<T>. A constant slice cannot be changed at all (it is read-only),ToArray()gives a normal copy. - The elements are stored once in static memory: using a constant slice copies and allocates nothing.
- An index or a range outside the slice is a compile error.
- A
threadfunction may read global constants, constant slices included. As a parameter of a thread, a constant slice is copied like anyReadOnlySlice<T>. Enum<T>.ValuesandEnum<T>.Namesare constant slices as well.
Embedded files: embed, embed_filenames and embed_lines
Section titled “Embedded files: embed, embed_filenames and embed_lines”embed("file") reads a file when the program is compiled and makes its content a string constant - for shaders,
translations, version files and the like:
const string Shader = embed("shaders/sprite.glsl");const string Version = embed("version.txt");With a wildcard (* for any characters, ? for one) in the file name, embed reads all matching files and gives
their contents as a constant ReadOnlySlice<string>, sorted by file name. embed_filenames gives the names of the
same files (without the folder), in the same order, so the two slices belong together:
const ReadOnlySlice<string> Levels = embed("levels/*.txt");const ReadOnlySlice<string> LevelNames = embed_filenames("levels/*.txt"); // "1-intro.txt", "2-cave.txt", ...
for (var i = 0; i < Levels.Length; i += 1) Console.WriteLine(LevelNames[i] + ": " + Levels[i].Length.ToString() + " characters");Declared as const ReadOnlySlice<uint8>, embed("file") gives the bytes of a file of any kind - fonts, images,
sounds, compressed data - exactly as they are on disk (a byte order mark, NUL bytes and bytes that are not UTF-8
included):
const ReadOnlySlice<uint8> Logo = embed("assets/logo.png");const ReadOnlySlice<uint8> Font = embed("assets/ui.ttf");
var logo = try Image.Decode(Logo); // decoded at run time, from the program's own dataThe bytes are written into the program as one block (no copy, no allocation at run time), like a string literal. The
constant is an ordinary constant slice: Logo.Length, Logo[0] and Logo[..8] are constants too. It reads one file:
a wildcard is an error, since a constant slice cannot contain slices.
embed_lines reads one file and gives its lines as a constant ReadOnlySlice<string> - word lists, maps of a level,
the rows of a sprite:
const ReadOnlySlice<string> Words = embed_lines("words.txt");const ReadOnlySlice<string> Ship = embed_lines("ship.txt"); // "..11..", ".1221.", ...
Console.WriteLine(Words.Length.ToString() + " words, the first is " + Words[0]);The lines are without their line ends (\n or \r\n, so a file checked out with Windows line ends gives the same
lines); a line end at the end of the file does not start another, empty line, and an empty file has no lines. A
wildcard is an error: for several files use embed("*.txt").
- Result type: with a wildcard it is always
const ReadOnlySlice<string>(empty if no file matches); without one it isconst string(embed_filenames("x.txt")gives"x.txt"and checks that the file exists), orconst ReadOnlySlice<uint8>for the bytes of the file.embed_linesis alwaysconst ReadOnlySlice<string>. The constant must be declared with exactly this type. - Wildcards only in the file name:
embed("data/*.json")is fine,embed("*/a.json")is an error. Folders never match, and subfolders are not searched. - Only like this:
embed(...)/embed_filenames(...)/embed_lines(...)is the whole initializer of a constant (top level or local). It does not exist at run time, cannot be part of a larger expression, and its argument must be a string literal. The constants are ordinary constants afterwards:[..LevelNames, "extra.txt"]is a constant slice too. - Exact content: the constant holds the file’s text unchanged - quotes, backslashes,
\r\nand\nstay as they are (only a UTF-8 byte order mark is dropped), soFile.WriteAllText(path, Shader)writes the same content. The file must be UTF-8 text; for any other file useconst ReadOnlySlice<uint8>, which keeps every byte. - Where the file is searched: an absolute path is used as it is. Otherwise the path is relative to the source file
that contains
embed, and if the file is not there, relative to the project folder (the folder ofcshift.json). A missing file is a compile error that lists where it was looked for. For a wildcard the folder is searched the same way: the first folder that has a matching file is used. - The file is read on every build, so a change to it is picked up the next time the program is compiled.
embed,embed_filenamesandembed_linesare keywords, so they cannot be used as names.
Global variables
Section titled “Global variables”A global variable is declared at the top level, with any type, with or without an initializer:
int RequestCount; // starts at 0 (the zero value)List<string> Log = List<string>.Create(); // an arbitrary initializer expression
void RecordRequest(){ RequestCount += 1; Log.Add("request " + RequestCount.ToString());}Initializers run before Main, in declaration order (and, across files, in the order the files are given to the
compiler). The compiler checks that order: an initializer must not read a global that’s initialized later, or
itself — including indirectly, through a function it calls:
int First = Second + 1; // compile error: uses Second before it is initializedint Second = 5;A global with no initializer can be read at any time, since its zero value is already well-defined. Globals are
ordinary lvalues — assign to them, pass with ref, take their address in unsafe, call methods on them, modify
their fields — and name resolution follows the same rules as for constants (the file’s namespace, using, or
qualified as Ns.Counter). Values that own heap memory (strings, arrays, lists, …) are released once Main
returns. There’s no var for a global (the type has to be written out), and reference counts aren’t atomic, so
globals aren’t safe to share across threads.
Next: C interop (FFI).