From e12cdcd7af4590f0f0ba555094022b399c85a6dd Mon Sep 17 00:00:00 2001 From: Brad Sharp <22573845+bradsharp@users.noreply.github.com> Date: Fri, 9 Jan 2026 14:29:43 -0800 Subject: [PATCH 1/3] Add RFC for the const keyword This document introduces the `const` keyword for local variable bindings, explaining its syntax, semantics, and potential drawbacks. --- docs/const-keyword.md | 128 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 docs/const-keyword.md diff --git a/docs/const-keyword.md b/docs/const-keyword.md new file mode 100644 index 000000000..fdb0a4607 --- /dev/null +++ b/docs/const-keyword.md @@ -0,0 +1,128 @@ +# 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 writing code, many variables are declared once and never intended to change such as module imports, function declarations, service references, and configuration objects. However, there is no way to prevent them from being rebound, which can lead to accidental rebinding, more time spent validating variables do not change during code reviews, and reducing the effectiveness of certain compiler / typechecker optimizations + +Additionally, with the proposal of the [export keyword](https://github.com/luau-lang/rfcs/pull/42), variables declared with it are frozen in the module exports table and therefore expected to refer to stable values. Without const-ness they can accidentally be redefined in confusing ways, within the module definition. + +## 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` becomes a keyword in the `local` declaration position. If `const` is currently allowed as an identifier, this is a source-breaking change only for code that uses const as a local variable name in positions where a local declaration is parsed. (See Drawbacks.) + +## Drawbacks + +- Keyword and compatibility surface: if `const` is currently used as an identifier in existing code, reserving it as a keyword can be source-breaking in some contexts. +- 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. + +## 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. From 0fb0c1108be66e60f6c10e36118bb97b474f8d59 Mon Sep 17 00:00:00 2001 From: Brad Sharp <22573845+bradsharp@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:29:08 -0800 Subject: [PATCH 2/3] Focus more on `export` in const keyword RFC Expand on the motivation for introducing the `const` keyword, emphasizing its role in ensuring stability for exported variables and preventing accidental reassignment. Provide examples illustrating the implications of mutable bindings in module exports. --- docs/const-keyword.md | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/const-keyword.md b/docs/const-keyword.md index fdb0a4607..5854d4c87 100644 --- a/docs/const-keyword.md +++ b/docs/const-keyword.md @@ -6,9 +6,33 @@ Introduce a `const` keyword for local variable bindings that prevents reassignme ## Motivation -When writing code, many variables are declared once and never intended to change such as module imports, function declarations, service references, and configuration objects. However, there is no way to prevent them from being rebound, which can lead to accidental rebinding, more time spent validating variables do not change during code reviews, and reducing the effectiveness of certain compiler / typechecker optimizations +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: -Additionally, with the proposal of the [export keyword](https://github.com/luau-lang/rfcs/pull/42), variables declared with it are frozen in the module exports table and therefore expected to refer to stable values. Without const-ness they can accidentally be redefined in confusing ways, within the module definition. +```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 From 6c28c15ac9353134ac0d8827e22e0d62a7e0a5f6 Mon Sep 17 00:00:00 2001 From: Brad Sharp <22573845+bradsharp@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:11:23 -0800 Subject: [PATCH 3/3] Update documentation on 'const' keyword usage Clarify the behavior and implications of the 'const' keyword in local declarations, including its contextual usage and potential drawbacks. --- docs/const-keyword.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/const-keyword.md b/docs/const-keyword.md index 5854d4c87..89f93615e 100644 --- a/docs/const-keyword.md +++ b/docs/const-keyword.md @@ -137,12 +137,12 @@ end ### Grammar / Parsing -`const` becomes a keyword in the `local` declaration position. If `const` is currently allowed as an identifier, this is a source-breaking change only for code that uses const as a local variable name in positions where a local declaration is parsed. (See Drawbacks.) +`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 -- Keyword and compatibility surface: if `const` is currently used as an identifier in existing code, reserving it as a keyword can be source-breaking in some contexts. - 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