luau/rfcs
Arseny Kapoulkine 1c6a7e61a6
RFC: Do not implement safe navigation operator
This meta-RFC proposes removing the previously accepted RFC on safe navigation operator. This is probably going to be disappointing but I feel like this is the best course of action that reflects our ideals in language evolution.

## Problem

The RFC specifies a new navigation operator, `?.`, that returns `nil` when the left hand side is `nil`, as well as skipping the rest of the indexed chain evaluation (e.g. `vehicle.Turret?.Frame.CFrame.LookVector` evaluates to `nil` if `vehicle.Turret` is `nil`). Initially the RFC only specifies this one operator, although future extensions like `?:`, `?[]`, `?()` are possible -- the RFC stands on its own, but it opens the path to more "nil-safe" operators in the future.

Unfortunately, we discovered a significant problem (after the RFC was merged) in the operator, in its interaction with the Roblox instance hierarchy. This is a userdata based DOM tree that (unfortunately) overloads __index to access children, and (fortunately?) attempting to access a non-existent child raises an error. As such, it's not compatible with this proposal in the sense that trying to use `?.` in combination with the instance hierarchy will only work if the child is present, and will raise an error otherwise.

## Alternatives

There are several ways to address that problem and still ship this; all of them are unsatisfactory:

1. We can change the instance hierarchy at Roblox to return `nil` instead. If Luau existed 15 years ago with `?.` feature, we probably would not have implemented dot-access in the first place, or maybe had it return `nil`... However, at present this is a very significant change that very dramatically changes the contract around instance access and may have large unintended consequences. It's something that isn't obviously a good idea in the first place, and not something we'd want to do for a single language feature as other features may pull us into maintaining the status quo instead.

2. We can ignore this footgun and/or try to help with static analysis. The issue here is that static analysis must depend on us knowing the precise type and tree location of the first object in the chain, something that will very often not work reliably, and the pattern is too convenient and too common - so we should probably admit that with or without analysis, people will be hitting this often accidentally. This can be treated as a reasonable compromise - we're adding a convenient feature that requires a little care around some constructs, and as long as the users "know what they are doing" it's going to be okay - but my perspective is that we should try to keep the language as free of footguns / as orthogonal / as simple as possible, and any feature that has significant issues around these must be a critical feature we can't live without for us to tolerate these.

3. We can introduce a new metamethod, `__safeindex`, that will be called for `?.` instead of `__index`, if present. This metamethod can then be defined for Roblox instance hierarchy to return `nil`. My perspective here is that while it removes the footgun, it violates the orthogonality / simplicity of the null safety operators in general - just for this one operator it now requires new VM opcodes, new table lookup functions that use either `__safeindex` or `__index` metamethods, and creates questions for any future null safety operators wrt whether or not we will go through the same process for consistency (eventually yielding `__safecall`, `__safenewindex`, `__safeadd` etc. in the extreme), or if `__safeindex` is a one-off ad hoc addition to solve a specific problem we have for one of the (well, largest) users of Luau language. This turns the safe navigation operator from "cheap convenient syntax sugar" to "full blown operator every element of the stack must be aware of", which - again, in my perspective - shifts the balance for this feature, as the costs no longer seem to justify the benefit.

## Conclusion

While we have ways forward for this proposal, they either make the language less robust [when used in Roblox environment], mean this feature is much less orthogonal/simple than initially understood, or mean we need to dramatically change the access rules [in Roblox environment] for a single small language feature.

While it's unusual to judge a feature of a general purpose language based on a single user of the said language, given Luau's heritage and the fact that the majority of the programmers who interact with Luau presently use it in context of Roblox, I think we must take this into account. These situations will probably happen rarely - in fact it's the first feature proposal like this! - but when they do, we should strive to keep the language simple and devoid of footguns, thus I believe it's overall beneficial to maintain the status quo and not implement this proposal.

As suggested in the RFC, this still means you can use `and` operator as a replacement for very basic single-element chains, e.g. `dog and dog.name`, but for more complex chains with `nil`s unfortunately the existing longer patterns must be used. We plan to look into common subexpression elimination that, under certain conditions, will allow us to maximally efficiently evaluate seemingly redundant expressions like `foo and foo.bar and foo.bar.baz`, and if that doesn't work you'd need to settle for multiple expressions to evaluate these chains efficiently in presence of nils - that said, I think in this case shorter code for some cases like this is not a strong enough motivation to ship the feature in the face of the problem discussed above.

cc @Kampfkarren for visibility
2022-05-24 16:31:04 -07:00
..
behavior-eq-metamethod.md Add RFC status fields (#39) 2021-05-17 18:19:49 -07:00
change-global-version.md Add RFC status fields (#39) 2021-05-17 18:19:49 -07:00
config-luaurc.md Mark RFCs that were implemented as such. 2021-11-03 21:35:25 -07:00
deprecate-getfenv-setfenv.md RFC: Deprecate getfenv/setfenv (#51) 2021-06-24 23:02:57 -07:00
function-bit32-countlz-countrz.md Mark singleton types and unsealed table literals RFCs as implemented (#438) 2022-03-29 16:58:59 -07:00
function-coroutine-close.md Mark singleton types and unsealed table literals RFCs as implemented (#438) 2022-03-29 16:58:59 -07:00
function-debug-info.md Spelling (#119) 2021-11-04 09:50:46 -05:00
function-string-pack-unpack.md Add RFC status fields (#39) 2021-05-17 18:19:49 -07:00
function-table-clear.md Add RFC status fields (#39) 2021-05-17 18:19:49 -07:00
function-table-clone.md Mark table.clone as implemented 2022-03-24 09:29:20 -07:00
function-table-create-find.md Add RFC status fields (#39) 2021-05-17 18:19:49 -07:00
function-table-freeze.md Spelling (#119) 2021-11-04 09:50:46 -05:00
generalized-iteration.md RFC: Generalized iteration (#335) 2022-02-14 10:04:07 -08:00
generic-functions.md Add turbofish discussion to generic function RFC (#300) 2022-01-07 11:07:36 -08:00
lower-bounds-calculation.md RFC: Lower Bounds Calculation (#388) 2022-03-29 12:37:14 -07:00
property-readonly.md RFC: Read-only properties (#77) 2021-10-11 09:58:01 -05:00
property-writeonly.md RFC: Write-only properties (#79) 2021-10-27 11:42:17 -07:00
README.md Remove team restriction from RFC process documentation 2021-11-03 12:13:42 -07:00
recursive-type-restriction.md Mark RFCs that were implemented as such. 2021-11-03 21:35:25 -07:00
sealed-table-subtyping.md Mark sealed table subtyping RFC as implemented 2022-03-24 13:10:30 -07:00
STATUS.md Update STATUS.md 2022-05-12 10:08:36 -07:00
syntax-array-like-table-types.md Add RFC status fields (#39) 2021-05-17 18:19:49 -07:00
syntax-compound-assignment.md Add RFC status fields (#39) 2021-05-17 18:19:49 -07:00
syntax-continue-statement.md Spelling (#119) 2021-11-04 09:50:46 -05:00
syntax-default-type-alias-type-parameters.md Mark default type parameters RFC as implemented (#369) 2022-02-17 16:14:35 -08:00
syntax-if-expression.md Fold in rationale for making else branch mandatory in if-then-else expressions. (#426) 2022-03-23 16:53:52 -07:00
syntax-named-function-type-args.md Mark 'Named Function Type Arguments' status as implemented (#41) 2021-05-31 21:07:37 +03:00
syntax-number-literals.md Add RFC status fields (#39) 2021-05-17 18:19:49 -07:00
syntax-singleton-types.md Mark singleton types and unsealed table literals RFCs as implemented (#438) 2022-03-29 16:58:59 -07:00
syntax-string-interpolation.md RFC: String interpolation (#165) 2021-11-22 14:59:38 -08:00
syntax-type-alias-type-packs.md Mark 'Type alias type packs' RFC as implemented (#237) 2021-11-23 10:03:20 -08:00
syntax-type-ascription-bidi.md Mark singleton types and unsealed table literals RFCs as implemented (#438) 2022-03-29 16:58:59 -07:00
syntax-type-ascription.md Add RFC status fields (#39) 2021-05-17 18:19:49 -07:00
syntax-typed-variadics.md Add RFC status fields (#39) 2021-05-17 18:19:49 -07:00
TEMPLATE.md Update TEMPLATE.md 2021-05-06 19:29:16 -07:00
unsealed-table-assign-optional-property.md Mark singleton types and unsealed table literals RFCs as implemented (#438) 2022-03-29 16:58:59 -07:00
unsealed-table-literals.md Mark singleton types and unsealed table literals RFCs as implemented (#438) 2022-03-29 16:58:59 -07:00
unsealed-table-subtyping-strips-optional-properties.md Mark last table subtyping RFC as implemented 2022-05-12 10:08:10 -07:00

Background

Whenever Luau language changes its syntax or semantics (including behavior of builtin libraries), we need to consider many implications of the changes.

Whenever new syntax is introduced, we need to ask:

  • Is it backwards compatible?
  • Is it easy for machines and humans to parse?
  • Does it create grammar ambiguities for current and future syntax?
  • Is it stylistically coherent with the rest of the language?
  • Does it present challenges with editor integration like autocomplete?

For changes in semantics, we should be asking:

  • Is behavior easy to understand and non-surprising?
  • Can it be implemented performantly today?
  • Can it be sandboxed assuming malicious usage?
  • Is it compatible with type checking and other forms of static analysis?

In addition to these questions, we also need to consider that every addition carries a cost, and too many features will result in a language that is harder to learn, harder to implement and ensure consistent implementation quality throughout, slower, etc. In addition, any language is greater than the sum of its parts and features often have non-intuitive interactions with each other.

Since reversing these decisions is incredibly costly and can be impossible due to backwards compatibility implications, all user facing changes to Luau language and core libraries must go through an RFC process.

Process

To open an RFC, a Pull Request must be opened which creates a new Markdown file in rfcs/ folder. The RFCs should follow the template rfcs/TEMPLATE.md, and should have a file name that is a short human readable description of the feature (using lowercase alphanumeric characters and dashes only). Try using the general area of the RFC as a prefix, e.g. syntax-generic-functions.md or function-debug-info.md.

Please make sure to add rfc label to PRs before creating them! This makes sure that our automatic notifications work correctly.

Every open RFC will be open for at least two calendar weeks. This is to make sure that there is sufficient time to review the proposal and raise concerns or suggest improvements. The discussion points should be reflected on the PR comments; when discussion happens outside of the comment stream, the points salient to the RFC should be summarized as a followup.

When the initial comment period expires, the RFC can be merged if there's consensus that the change is important and that the details of the syntax/semantics presented are workable. The decision to merge the RFC is made by the Luau team.

When revisions on the RFC text that affect syntax/semantics are suggested, they need to be incorporated before a RFC is merged; a merged RFC represents a maximally accurate version of the language change that is going to be implemented.

In some cases RFCs may contain conditional compatibility clauses. E.g. there are cases where a change is potentially not backwards compatible, but is believed to be substantially beneficial that it can be implemented if, in practice, the backwards compatibility implications are minimal. As a strawman example, if we wanted to introduce a non-context-specific keyword globallycoherent, we would be able to do so if our analysis of Luau code (based on the Roblox platform at the moment) informs us that no script in existence uses this keyword. In cases like this an RFC may need to be revised after the initial implementation attempt based on the data that we gather.

In general, RFCs can also be updated after merging to make the language of the RFC more clear, but should not change their meaning. When a new feature is built on top of an existing feature that has an RFC, a new RFC should be created instead of editing an existing RFC.

When there's no consensus that the feature is broadly beneficial and can be implemented, an RFC will be closed. The decision to close the RFC is made by the Luau team.

Note that in some cases an RFC may be closed because we don't have sufficient data or believe that at this point in time, the stars do not line up sufficiently for this change to be worthwhile, but this doesn't mean that it may never be considered again; an RFC PR may be reopened if new data is available since the original discussion, or if the PR has changed substantially to address the core problems raised in the prior round.

Implementation

When an RFC gets merged, the feature can be implemented; however, there's no set timeline for that implementation. In some cases implementation may land in a matter of days after an RFC is merged, in some it may take months.

To avoid having permanently stale RFCs, in rare cases Luau team can remove a previously merged RFC when the landscape is believed to change enough for a feature like this to warrant further discussion.

When an RFC is implemented and the implementation is enabled via feature flags, RFC should be updated to include "Status: Implemented" at the top level (before Summary section).