Closed rbuckton closed 1 month ago
I like the lexical semantics of this a lot.
Bikeshedding-wise, unsafe
is a pretty broad descriptor. On the one hand, it seems reasonable to imagine lumping other features we deem unsafe into this safe/unsafe divide in the future. On the other hand, it is also reasonable to consider thread safety to be unique and have a syntactic marker narrowly scoped to allowing possibly thread unsafe code, in which case a different word than unsafe
would better capture the intent.
Writing down comments from the working session:
It's not clear that the addition of additional unsafe contexts (beyond the bare unsafe { ... }
block syntax) is necessary. There seems to be general consensus that the block syntax is a good idea, but the majority of the participants in the call felt that additional syntax could wait until we had more developer feedback indicating that the user experience of function foo() { unsafe { ... } }
was enough worse than unsafe function foo() { ... }
to justify adding additional complexity.
Given that function colouring is an explicit non-goal of this proposal, I would further argue that unsafe function foo() {...}
is potentially misleading. Existing modifiers to function declarations (async
and *
) affect the return type of the function in a way that matters to the caller.
This proposal is inspired by Rust's unsafe keyword. But note that in Rust, unsafe function foo() {}
defines a function that can only be called from an unsafe context. That enables Rust programmers to precisely specify which code is responsible for maintaining safety invariants (although see this for an explanation of why Rust unsafety actually pollutes code out to the module boundary), but validating it at runtime would impose additional requirements on implementations.
For example, this version of unsafe functions enables idioms like unsafe function foo_alreadyHoldingLock() {...}
that impose safety requirements on the caller.
As a very different language from Rust, it's unclear whether JS would benefit from a similar kind of function colouring. By restricting the initial proposal to only support unsafe blocks, we leave flexibility to decide based on user experience whether we want function colouring or not.
For these reasons, I think we should remove "Other unsafe contexts" from this proposal.
This has also landed in the spec draft, so closing. Feel free to open up more specific issues regarding unsafe blocks.
During the last Shared Structs call we discussed concerns raised about there being insufficient guardrails in place to prevent users from inadvertently introducing data races by interacting with shared data structures outside of some mechanism to synchronize access.
Non-thread-safe (e.g., "unsafe") Code Contexts
As a means to address these concerns, I propose that we limit read/write access to shared data to code running inside of a clearly labeled "unsafe" context, such as an
unsafe {}
block:How is
unsafe
a "guardrail"?The
unsafe
keyword is a clear signal of intent that a developer is choosing to work with shared memory multithreaded code. The presence of anunsafe
block is an indication to code reviewers that special care must be taken during review. It also is acts as a syntactic marker that future tooling (linters, type checkers, etc.) could use to identify data races.The
unsafe {}
block and Block SemanticsAn
unsafe {}
block is otherwise treated the same as a normal Block. Its only distinction is that it explicitly labels code within the block as potentially containing non-thread-safe (e.g., "unsafe") code. The general expectation is that any thread safety concerns should be addressed by the developer as control flow exits theunsafe
block. For example, you could utilizeusing
to synchronize access to a shared struct via a lock:Here, when the control enters the
unsafe
block, we allocate a lock against the provided mutex via ausing
declaration. As control exits theunsafe
block, the lock tracked byusing
is released.What does
unsafe
do?The
unsafe
keyword is a syntactic marker that applies to lexically scoped reads and writes of the fields of a shared struct instance. Within anunsafe
block, any lexically scoped accesses are permitted, even if they are nested within another function declared in the same block. This special lexical context shares some surface level similarities with the lexical scoping rules for private names, or the runtime semantics of"use strict"
.Since
unsafe
is lexically scoped, it does not carry over to the invocation of functions declared outside of anunsafe
context:Calling into, and out of,
unsafe
codeThread-safe code may execute
unsafe
code without restriction, andunsafe
code may do likewise. Asunsafe
already indicates a transition boundary between thread-safe and unsafe code, there is no need to declare all calling codeunsafe
as you might need to do forasync
/await
. Theunsafe
keyword itself does not entail any implicit synchronization or coordination as that would be in opposition to our performance goals. Instead, the onus is on developers to be cognizant of thread safety concerns when they define anunsafe
block. As such, a developer can choose the coordination mechanism that best suits the needs of their application, be that aMutex
, a lock-free concurrent deque, etc.Other
unsafe
ContextsIt should be noted that this proposal is not limited to merely
unsafe {}
, but also proposes the addition ofunsafe
as a contextual keyword in a number of other contexts so as to improve the developer experience. As such I also propose the following as possibleunsafe
contexts:unsafe {}
(as discussed above)unsafe function f() {}
(orfunction f() unsafe {}
, etc.)unsafe () => { }
unsafe async function f() {}
,unsafe function* g() {}
, etc.unsafe m() {}
unsafe get value() { ... }
unsafe constructor() {}
(though ashared struct
constructor might implicitly beunsafe
)unsafe x = ...;
static
blocks —unsafe static { }
class
/struct
/shared
struct bodies —unsafe shared struct S { ... }
unsafe const x = ...
(to avoid needing anunsafe
IIFE, splitting into alet
, etc.)There may also be other contexts we may wish to consider in the future as well.
Examples
Atomic values
Worker Coordination