Skip to content

Const Keyword for Local Declarations#166

Merged
SPY merged 3 commits intomasterfrom
rfc-const-keyword
Feb 24, 2026
Merged

Const Keyword for Local Declarations#166
SPY merged 3 commits intomasterfrom
rfc-const-keyword

Conversation

@bradsharp
Copy link
Contributor

@bradsharp bradsharp commented Jan 9, 2026

This document introduces the const keyword for local variable bindings, explaining its syntax, semantics, and potential drawbacks.

Rendered

This document introduces the `const` keyword for local variable bindings, explaining its syntax, semantics, and potential drawbacks.
@jackdotink
Copy link
Contributor

The motivation is very weak and the drawbacks of breaking backwards compatibility are... bad.

The motivation boils down into preventing accidental reassignment, and enabling unspecified implementation optimizations. Accidental reassignments by and large just don't happen. As for the unspecified optimizations, luau today knows if a variable is constant or not, and if it is, it applies optimizations. Checking if a binding is ever reassigned is trivial.

I assume the suggestion of breaking backwards compatibility comes with the idea of a tool to allow users to migrate code. This is bad because such a tool is infeasible for the largest user of the language, roblox. The migration tool cannot be run at user's own leisure because roblox forces the latest version of luau, and the tool cannot be run automatically effectively because many users do not use roblox as a source of truth for their code.

All in all, this feature opens too many difficult questions for the lack of utility it provides.

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.
Clarify the behavior and implications of the 'const' keyword in local declarations, including its contextual usage and potential drawbacks.
@bradsharp
Copy link
Contributor Author

bradsharp commented Jan 12, 2026

The motivation is very weak

export is the primary motivation for introducing const-ness to the language. When we return from a module with exports we freeze the result (see why here). This leads to an issue where the variables can be reassigned but the return value itself doesn't change. If we're going to introduce const-ness to the language for export then it seems reasonable to expose it for general use at the same time.

I've added more details about this to the RFC.

the drawbacks of breaking backwards compatibility are... bad.

Instead of reserving const as a reserved keyword it can be introduced as a contextual keyword that is only valid in positions that local is also valid. This makes the change compatible with existing code as const foo = 5 would fail to parse today.

I've covered this in the RFC also.


```luau
const x = 1
x = 2 -- error
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of error? Static(Parsing Error) or dynamic?
Difference is more obvious inside a function definition.

const a = 42
function foo() -- 1
  a = 43
end
foo() -- 2

Should we see an error at parsing of 1 or at the call of 2?
JavaScript picks option 2, but honestly I'm not aware of advantage of it. So I would prefer option 1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it can be implemented as a parsing error but we may want a runtime error as a backstop for cases like loadstring. I would expect the error to be on the line of the actual assignment, in this case a = 43.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an aside JavaScript likely picks (2) because it's the line that leads to a being mutated and without it the value remains constant. For sanity, I think we should stick to erroring on a = 43 even if the code is unreachable.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JS goes crazier here:

const a = 42;

function bar(overrides) {
  with (overrides) {
    let foo = function() {
      console.log(a);
      a = 43;
      console.log(a);
    }

    foo();
  }
}

bar({ a: 50 }); // prints 50, 43
bar({ b: 50 }); // prints 42 and throws TypeError: Assignment to constant variable.


## 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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned above it is not an issue right now.
But it can be in conflict with developing proposals, like destructive assignment.
Theoretically you can have something like that in you code now:

const {a, b}

and it sould be treated as a call to const with a single param.
Assuming destructive assignment will look like

const {a, b} = x

It can be an issue

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As const variables must always be initialized const {a, b} would be invalid (and therefore treated as a function call) while const {a, b} = x remains unambiguous as this is invalid syntax today. The one downside I see if that this could become a gotcha, so we might want to consider a linter warning for const {a, b}.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradsharp While const {a, b] = x is not ambiguous, it is still not reasonably parsable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It almost makes one want for a "Do not add destructuring syntax" RFC

const x: number = 5
const t: { a: number } = { a = 1 }
```
Multi-assignment is also supported:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth mentioning vararg multi assignment explicitly.

it should be fine from runtime point of view to have something like

function f() return 1 end

const a, b = f()

Because f can return multiple values in last position(same with ...). In that case b should be initialized with nil. But typechecker probably should complain here.

Copy link

@SPY SPY left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think RFC in a good shape and can be merged.

@MagmaBurnsV
Copy link
Contributor

There's 9 thumbs down. Shouldn't we wait for more community input?

A major problem with this RFC is that it's primarily motivated on fixing a hole caused by the current semantics of the export-by-value RFC. This motivation is entirely speculative and different semantics for export-by-value can and should be chosen to solve this instead.

Secondary motivation for safeguarding reassignment is also rather unpopular. Anecdotal experience from myself and others says that accidental local reassignments rarely happen, and more often we want cross-module immutability, which is taken care of with table.freeze.

@jackdotink
Copy link
Contributor

I'm more concerned on the future compatibility concerns with destructuring syntax, as in, this RFC doesn't leave a path open for destructuring syntax.

@andyfriesen
Copy link
Collaborator

Something this RFC doesn't mention is that export (and therefore const) are partly motivated by optimization opportunities that we'd like to pursue. It's very easy for us to do constant folding and function inlining when we have a language-level guarantee that a symbol can never be rebound.

A const variable is equivalent to a variable declared with local however const variables cannot be reassigned after they are initialized.

It would be good to clarify that attempting to rebind a const binding also results in a runtime exception.

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.

It must be said that JavaScript has been living with this restriction for years now and it's not a huge problem.

@SPY SPY merged commit 3ebffa2 into master Feb 24, 2026
@SPY
Copy link

SPY commented Feb 24, 2026

Merged as a dependency for #42

@MagmaBurnsV
Copy link
Contributor

I must strongly object to merging this RFC.

First, I think it is dishonest to say this is needed as a "dependency" for the export-by-value RFC. This is not true and even contradicts the RFC's stated motivation:

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.

Nothing about exporting needs this, and it shouldn't be framed like that.

Second, as I've already said before, this motivation is very weak as its only substantial rationale is to close a feature gap caused by the current export-by-value semantics. This can instead be solved by adopting different export semantics that don't cause feature gaps, such as my proposed alternative semantics.

Even if the team is convinced that current export semantics are the best for static optimizations (I disagree, my alternative can also obtain these optimizations without introducing const-ness), that still doesn't justify implementing const before export-by-value has even been accepted.

@alexmccord
Copy link
Contributor

alexmccord commented Feb 26, 2026

Rebinding const can all be done at parse time, there's no need for a runtime error to rebind const. loadstring can only affect globals, which const isn't, it occupies a register (modulo const-of-const elimination) which do not carry into loadstring. See local x = 5 assert(loadstring("x = 7; print(x)"))() print(x) which prints 7 then 5.

aatxe added a commit to luau-lang/luau that referenced this pull request Mar 6, 2026
Hi there, folks! We're back with another weekly Luau release!

# Language

* Adds the `const` keyword for defining constant bindings that are
statically forbidden to be reassigned to. This implements
[luau-lang/rfcs#166](luau-lang/rfcs#166).
* Adds a collection of new math constants to Luau's `math` library per
[luau-lang/rfcs#169](luau-lang/rfcs#169).

# Analysis

* Fixes a class of bugs where Luau would not retain reasonable upper or
lower bounds on free types, resulting in types snapping to `never` or
`unknown` despite having bounds.
```luau
--!strict
-- `lines` will be inferred to be of `{ string }` now, and prior
-- was 
local lines = {}
table.insert(lines, table.concat({}, ""))
print(table.concat(lines, "\n"))
```
```luau
--!strict
-- `buttons` will be inferred to be of type `{ { a: number } }`
local buttons = {}
table.insert(buttons, { a = 1 })
table.insert(buttons, { a = 2, b = true })
table.insert(buttons, { a = 3 })
```
* Disables the type error from `string.format` when called with a
dynamically-determined format string (i.e. a non-literal string argument
with the type `string`) in response to user feedback about it being too
noisy.
* Resolves an ICE that could occur when type checking curried generic
functions. Fixes #2061!
* Fixes false positive type errors from doing equality or inequality
against `nil` when indexing from a table
* In #2256, adds a state parameter to the `useratom` callback for
consistency with other callbacks.

# Compiler
- Improves the compiler's type inference for vector component access,
numerical for loops, function return types and singleton type
annotations, fixing #2244 #2235 and #2255.

# Native Code Generation

- Fixes a bug where some operations on x86_64 would produce integers
that would take up more than 32-bits when a 32-bit integer is expected.
We resolve these issues by properly truncating to 32-bits in these
situations.
- Improves dead store elimination for conditional jumps and fastcalls
arguments, improving overall native codegen performance by about 2% on
average in benchmarks, with some benchmarks as high as 25%.

---------

Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants