Skip to content
CShift0.32

Constants and global variables

← Language guide

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 allowed
const Color Favorite = Color.Green; // enum members too
const int Doubled = MaxPlayers * 2; // other constants, operators, casts

Only 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 expression

The 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.

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 too
const int Count = Primes[1..].Length; // 3
const int Letters = "hello".Length; // (also for constant strings)
  • Arrays and Slice<T> cannot be constants, because their elements can be changed; the error suggests ReadOnlySlice<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 thread function may read global constants, constant slices included. As a parameter of a thread, a constant slice is copied like any ReadOnlySlice<T>.
  • Enum<T>.Values and Enum<T>.Names are 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 data

The 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 is const string (embed_filenames("x.txt") gives "x.txt" and checks that the file exists), or const ReadOnlySlice<uint8> for the bytes of the file. embed_lines is always const 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\n and \n stay as they are (only a UTF-8 byte order mark is dropped), so File.WriteAllText(path, Shader) writes the same content. The file must be UTF-8 text; for any other file use const 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 of cshift.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_filenames and embed_lines are keywords, so they cannot be used as names.

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 initialized
int 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).