mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
152 lines
5.4 KiB
Markdown
152 lines
5.4 KiB
Markdown
# 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 <const> = 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.
|