6. Constant Expressions

A constant expression (sometimes called a constexpr) is an expression that can be fully evaluated by the compiler at compile time. This feature is primarily for specifying the size of statically-sized arrays.

In Gazprea, a constexpr is not a keyword, but a property of a const variable. A const variable is considered a constexpr if and only if its initializer expression meets a strict set of criteria:

6.1. Rules for Constant Expressions

An expression is a valid constexpr if it is composed exclusively of:

  1. Literals of base types (boolean, integer, real, character).

  2. The operators +, -, *, /, not, and, or, between two or more constexprs.

  3. Constructors for aggregate types, provided that the aggregate is const and all members are constexprs.

  4. Index or field access on constexpr aggregate types.

  5. Other variables that are themselves valid constexprs.

An expression is not a constexpr if it contains:

  1. References to var variables.

  2. Function or procedure calls.

  3. Any I/O operations (<-).

The compiler must perform this validation recursively. When checking if a variable is a constexpr, the compiler must trace its entire dependency chain. If the chain ever depends on a runtime value, the check fails.

The only expressions that must be constexpr are global constants. Other constexprs arising from constants inside function scope may also be constexprs but the implementation does not need to enforce or necessarily identify this. Students should also note that MLIR has a constant propagation pass built in, so doing constant folding yourself may not be necessary depending on your implementation.

Examples:

Note: we will annotate the scope explicitly in these examples. Some ‘illegal’ examples here would be legal within a non-global scope.

// ----------------------------
// in global scope
// ----------------------------

// Legal Global Constant Expressions
const A = 10;
const B = A * 2; // Depends on another constexpr
const C = B + 5; // C is 25

// Illegal Global Constant Expressions
var x = 10;
const Y = x + 5; // Not a constexpr: depends on a 'var'

function get_val() returns integer { return 100; }
const Z = get_val(); // Not a constexpr: depends on a function call

6.2. Constant Expressions with Aggregate Types

Arrays and tuples can also be constexprs if they meet specific criteria, allowing them to be used to define other constants.

  1. Arrays

    A const array is a constexpr if:

    1. Its size is a valid constexpr.

    2. All of its element initializers are valid constexprs.

    A vector (the dynamically-sized type) can never be a constexpr aggregate, since its size is determined at runtime. An inferred-size array such as integer[*] X = [1, 2, 3] must be a constexpr, meaning its initializer is itself a constexpr: [*] denotes an inferred size, not a dynamic one.

    // ----------------------------
    // in global scope
    // ----------------------------
    
    const WIDTH = 5;
    const integer[WIDTH] LOOKUP_TABLE = [10, 20, 30, 40, 50]; // Legal constexpr array
    
    const ELEMENT = LOOKUP_TABLE[3];          // Legal: ELEMENT is a constexpr with value 30
    integer[ELEMENT] my_array = 0;            // Legal: static array of size 30, zero-filled
    
    const integer[2] BAD_TABLE = [10, get_val()]; // Illegal: initializer is not a constexpr
                                                  //  also illegal if a procedure since
                                                  //  procedure calls are not allowed
                                                  //  within declarations
    

    A constexpr can appear anywhere a const declaration is legal, including inside functions, procedures, and control-flow blocks. However, not every const variable is a constexpr. const means only that the variable is immutable within its scope; constexpr is the stronger property that the value is fully known at compile time. For example:

    // ----------------------------------
    // in local/function/non-global scope
    // ----------------------------------
    var integer x;
    x <- std_input;
    const integer y = x; // Legal: y is immutable, but NOT a constexpr
                         // because its value depends on runtime input.
    integer[y] arr;      // Illegal: an explicit array size must be a
                         // constexpr, and y is not a constexpr.
    vector<integer> v;   // Legal: a vector is the dynamically-sized type;
                         // use it when the size is only known at runtime.
    

    The compiler propagates the constexpr property through local scopes normally; there is no restriction on where in a block the declaration appears, as long as its entire dependency chain satisfies the rules above.

  2. Tuples

    A const tuple is a constexpr if all of its fields are initialized with valid constant expressions.

    // ----------------------------
    // in global scope
    // ----------------------------
    const CONFIG = (true, 10 * 2); // Legal constexpr tuple
    
    const IS_ENABLED = CONFIG.1; // Legal: IS_ENABLED is a constexpr with value 'true'
    const VALUE = CONFIG.2;      // Legal: VALUE is a constexpr with value 20