diff --git a/docs/const-keyword.md b/docs/const-keyword.md new file mode 100644 index 000000000..89f93615e --- /dev/null +++ b/docs/const-keyword.md @@ -0,0 +1,152 @@ +# Const Keyword + +## Summary + +Introduce a `const` keyword for local variable bindings that prevents reassignment after initialization. A const declaration makes the binding immutable, not the value, so tables and other mutable objects remain mutable through the binding. + +## Motivation + +When introducing the [`export`](https://github.com/luau-lang/rfcs/pull/42) keyword we will need to introduce the concept of `const`-ness to the language. Modules with exports return a frozen table of values; however, the variables used to declare those exports are not themselves immutable. This makes it possible to write code such as the following: + +```luau +export foo = 5 + +export function increment() + foo += 1 +end +``` + +This results in different values for `foo` depending on where it is accessed from: + +```luau +-- Inside the module +print(foo) --> 5 +increment() +print(foo) --> 6 + +-- Outside the module +print(module.foo) --> 5 +module.increment() +print(module.foo) --> 5 +``` + +To address this, we propose that all exported values are `const` by default, preventing reassignment within the module and ensuring that exported bindings represent stable values. + +However, this is not the only use-case for `const`. A `const` binding provides a clear guarantee that a name will always refer to the same value for the lifetime of its scope. This property is useful beyond module exports: it makes code easier to reason about, avoids accidental rebinding in closures or long-lived scopes, and allows tools and the typechecker to treat such bindings as stable references rather than values that may change over time. Since `export` already requires this guarantee to behave correctly, exposing `const` as a general language feature avoids introducing special-case rules and provides a simple, consistent way to express immutability of bindings where it is desired. + +## Design +### Syntax + +Allow `const` in declarations anywhere `local` is allowed: + +```luau +const x = 5 +const f = function() end +const t = { a = 1 } +``` + +Typed declarations remain valid: + +```luau +const x: number = 5 +const t: { a: number } = { a = 1 } +``` +Multi-assignment is also supported: + +```luau +const a, b = 1, 2 +``` + +A `function` can be declared as `const`: + +```luau +const function f() + -- do something +end +``` + +### Semantics + +A `const` variable is equivalent to a variable declared with `local` however `const` variables cannot be reassigned after they are initialized: + +```luau +const x = 1 +x = 2 -- error +``` + +This includes any assignment form that writes to the binding, such as compound assignment: + +```luau +const x = 1 +x += 1 -- error +``` + +It applies to the variable's binding, not to the value it refers to. If a const variable refers to a mutable value such as a table, that value can still be modified: + +```luau +const t = { count = 0 } +t.count += 1 -- ok (mutating the table) +t = {} -- error (reassigning the binding) +``` + +`const` is not a replacement for `table.freeze` which should be used to prohibit mutation of table contents. The two features address different problems and can be used together: + +```luau +const t = table.freeze({ + count = 0, +}) + +t.count += 1 -- error (mutating a frozen table) +t = {} -- error (reassigning the binding) +``` + +#### Initialization + +A `const` binding must be initialized at the point of declaration: + +``` +const x -- error: const variable must be initialized +``` + +This keeps the feature simple, avoids "definite assignment" rules, and makes the "never reassigned" intent explicit. + +#### Scope and Shadowing + +`const` follows normal lexical scoping rules. Shadowing creates a new binding and is allowed: + +```luau +const x = 1 +do + const x = 2 -- ok: new binding in inner scope +end + +const y = 1 +const y = 2 -- also ok: rebinding as const in same scope +``` + +#### Captures / Up-Values + +If a `const` variable is captured by an inner function, it remains non-reassignable from within that function as well: + +```luau +const x = 1 +const function f() + x = 2 -- error +end +``` + +### Grammar / Parsing + +`const` is a contextual keyword that is only valid in positions where `local` is valid. This makes the introduction fully backwards compatible with existing code. + +## Drawbacks + +- Potential false sense of immutability: developers may misread const as implying deep immutability. This is mitigated by clear documentation and examples, but the risk remains. +- Lua introduced `const` in the form `local foo = 5` which is inconsistent with this syntax. However, we've already decided we won't adopt Lua const syntax, therefore consider this an acceptable change. + +## Alternatives + +- Lint-only "never reassigned" rule: A linter can infer locals that are never reassigned and warn on future changes. This avoids new syntax, but loses the ability to declare intent directly in code (e.g., a new assignment becomes a warning rather than a clear violation of an explicit constraint). +- Deep immutability / freezing: Enforcing immutability of tables (recursively) would address mutation bugs but introduces complexity, runtime overhead, questions around metatables/userdata, and awkward interactions with existing patterns. This is explicitly out of scope for this proposal. +- Annotations instead of a keyword: An attribute-like syntax (comment directive or type-level modifier) could avoid reserving a keyword, but is less readable, less discoverable, and harder to standardize across tooling. +- Do nothing: Continue relying on convention and review. This keeps the language smaller but preserves a common footgun and prevents tooling from enforcing a widely useful invariant.