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

#!ek9
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: access, clone, constant, copy, deep, immutable, mutation, protection, safety