-
Notifications
You must be signed in to change notification settings - Fork 81
Const Keyword for Local Declarations #166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What kind of error? Static(Parsing Error) or dynamic? Should we see an error at parsing of 1 or at the call of 2?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As an aside
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| ``` | ||
|
|
||
| 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. | ||
There was a problem hiding this comment.
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
Because f can return multiple values in last position(same with
...). In that casebshould be initialized withnil. But typechecker probably should complain here.