Loft gives you two separate ways to protect a value from accidental changes:
1. **Compile-time const**: mark a variable or parameter with 'const' and the compiler refuses to compile any code that tries to reassign it. The check happens before the program even runs — zero runtime cost.
2. **Runtime lock**: the '#lock' attribute on a reference lets you lock a store at runtime so that any write attempt panics immediately, even across function boundaries. This is useful for debugging: turn it on when you suspect an unexpected mutation.
struct Counter {
value: integer
}
const parameters
'const' on a parameter is a compile-time promise: "this function will not modify this value." The compiler enforces it — any assignment to a const parameter is a compile error, caught before you run anything.
fn read_value(self: const Counter) -> integer {
self.value
}
A non-const parameter leaves the store unlocked so the function can write.
fn increment(self: Counter) {
self.value += 1
}
fn main() {
const local variables
Declare a local variable with 'const' to signal that it will not change after its first assignment. The compiler rejects any later assignment to it — reassigning or appending to a const variable is a compile error.
This is handy for configuration values or lookup tables that should never be overwritten by accident deep inside a long function.
const limit = 100;
assert(limit == 100, "const integer is readable");
const also works for struct references.
const cfg = Counter {value: 42 };
assert(cfg.value == 42, "const reference is readable");
Passing a const reference to a const parameter is always allowed.
assert(read_value(cfg) == 42, "const passed to const param");
Calling methods on const references
A non-const method can still be called on a non-const variable even after you have manually locked the store; the lock is a runtime check.
c = Counter {value: 10 };
increment(c);
assert(c.value == 11, "increment modified c");
Runtime store locks with #lock
'#lock' is an attribute on any reference variable. Setting it to true turns on a runtime guard: any write to that store will panic immediately, wherever it happens. A freshly created reference starts unlocked.
d = Counter {value: 5 };
assert(!d#lock, "new store starts unlocked");
d#lock = true;
assert(d#lock, "store is locked after assignment");
You can still read from a locked store — only writes are blocked.
assert(read_value(d) == 5, "locked store is still readable");
When to use each approach
* Use 'const' on parameters and locals to express your design intent and get compile-time safety at zero cost. * Use '#lock = true' when you want a runtime tripwire: you suspect some code path is mutating a value it should not touch, and you want the program to panic with a precise location rather than corrupt silently. get_store_lock() is the function form of the #lock attribute. Both return the same boolean.
e = Counter {value: 99 };
assert(get_store_lock(e) == e#lock, "function form matches attribute before lock");
e#lock = true;
assert(get_store_lock(e) == e#lock, "function form matches attribute after lock");
}