How does EK9 protect constants from mutation?

← Constants and Immutability · Ref: Q141

EK9 enforces deep immutability for constants through two mechanisms: copy-on-access and compile-time mutation blocking. Every reference to a constant produces an independent copy, and all mutation operators are blocked at compile time.

COPY-ON-ACCESS

Every time you reference a constant, the compiler generates code that loads the constant value and immediately creates a fresh copy via the type's copy constructor. This means each access gets an independent copy. If you write:

  msg <- greeting

The variable msg receives a copy of the greeting constant, not a reference to the same object.

DEEP IMMUTABILITY

All mutation operators are blocked on constants at compile time with error NOT_MUTABLE:

  ++  --  +=  :=  :=?  :=:  :~:  :^:

Attempting any of these on a constant produces a compile error. This is not a runtime check; the compiler catches it before any code is generated.

AUTO-CLONING ON PASS

When you pass a constant as a function argument, the compiler auto-clones it. The function receives a copy, so the original constant value is unaffected regardless of what the function does with its parameter.

AUTO-CLONING ON RETURN

When a constant is used in an expression or returned from a function, the result is always a fresh mutable copy. This means you can freely use constants in computations without risk.

VARIABLE COPIES ARE MUTABLE

Once you assign a constant to a variable, the variable holds an independent mutable copy:

  name <- greeting
  name += " World"

The variable name is mutable and can be modified. The greeting constant remains unchanged.

WHY ONLY BUILT-IN TYPES

User-defined classes and records cannot be constants because copy-on-access requires the compiler to know exactly how to copy the value. Built-in types have guaranteed copy constructors with well-defined semantics. For user-defined types, the compiler cannot guarantee that a copy is truly independent, so it restricts constants to the 18 built-in value types.

See Q140 for defining constants and the full list of constant-eligible types. See Q130 for mutable vs immutable collection operations. See Q29 for unset/set semantics and how constants are always set. See Q143 for how this compares to const/final/static in other languages. See Q268 for how immutability prevents OWASP data integrity vulnerabilities. See Q317 for magic literal detection that encourages named constants.

Example

defines module qa.constants.immutability

  defines constant
    greeting <- "Hello"
    baseCount <- 10
    price <- 9.99#USD

  defines function

    addSuffix()
      -> text as String
      <- rtn as String: text + " World"

    doubleIt()
      -> n as Integer
      <- rtn as Integer: n + n

  defines program

    ConstantImmutabilityDemo()
      stdout <- Stdout()

      // === COPY-ON-ACCESS: each reference is a fresh copy ===

      first <- greeting
      second <- greeting
      stdout.println(`First: ${first}`)
      stdout.println(`Second: ${second}`)

      // === VARIABLE COPIES ARE MUTABLE ===

      mutableName <- greeting
      mutableName += " World"
      stdout.println(`Modified copy: ${mutableName}`)

      // Original constant unchanged on next access
      stdout.println(`Constant still: ${greeting}`)

      // === CONSTANTS IN EXPRESSIONS ===

      doubled <- baseCount + baseCount
      stdout.println(`Doubled: ${doubled}`)
      stdout.println(`Original: ${baseCount}`)

      // === CONSTANTS PASSED TO FUNCTIONS ===

      result <- addSuffix(greeting)
      stdout.println(`Function result: ${result}`)
      stdout.println(`Constant after call: ${greeting}`)

      computed <- doubleIt(baseCount)
      stdout.println(`Computed: ${computed}`)
      stdout.println(`Base still: ${baseCount}`)

      // === CONSTANTS IN STRING INTERPOLATION ===

      stdout.println(`The price is ${price}`)

Common mistakes

E07890 — Constants are protected by copy-on-access immutability. You cannot use mutating operators like += on a constant. Use the constant in a non-mutating expression to produce a new value instead. See ek9 -h E07890 for details.

Incorrect:

baseCount += 10

Correct:

doubled <- baseCount + baseCount

E07890 — Constants cannot be reassigned with :=. Unlike Java final or JavaScript const which only prevent reassignment, EK9 constants block ALL mutation including assignment. Create a new variable from the constant instead. See ek9 -h E07890 for details.

Incorrect:

baseCount := 20

Correct:

computed <- doubleIt(baseCount)

E07890 — The ++ operator mutates the target variable. Constants cannot be mutated by any operator. Use the constant in a non-mutating expression to compute a new value. See ek9 -h E07890 for details.

Incorrect:

greeting++

Correct:

mutableName <- greeting
Other ways to ask this
  • Can I modify constants in EK9?
  • Why are constants copied on access?
  • How does copy-on-access work for constants?

Coming from another language?

Java: final prevents reassignment but not mutation (final List can still add()). JavaScript: const prevents reassignment but not mutation (const obj = {}; obj.x = 1 works). Rust: const is compile-time only, let bindings are immutable by default. Go: const limited to basic types. EK9: copy-on-access ensures every reference gets a fresh independent copy, plus all mutation operators are blocked at compile time. Truly immutable, no escape hatch.

Keywords: protection, access, constant, mutation, immutable, safety, deep, clone, copy