00:00
<shu>
(the magic from a spec perspective will be, like, look at the current parse node being evaluated, and then find the nearest enclosing block)
00:00
<rbuckton>
I don't necessarily think we need general purpose function coloring, though I do like the additional guardrail that provides. I'm primarily interested in just improving the DX of function f() { unsafe { and function f(x, y = do unsafe { x.y }) since those feel very awkward
00:01
<rbuckton>
(the magic from a spec perspective will be, like, look at the current parse node being evaluated, and then find the nearest enclosing block)
Wouldn't we just look at the current lexical environment as part of Call?
00:01
<Mathieu Hofman>
To inform this intrinsics call coloring question, I think the `Reflect.get` case is interesting. Would you expect `unsafe { Reflect.get(sharedStruct, "foo") }` to work?
00:01
<shu>
Wouldn't we just look at the current lexical environment as part of Call?
oh true, it'll always have one
00:02
<shu>
must be nice to have an implementation that never optimizes away scopes!
00:04
<rbuckton>
To inform this intrinsics call coloring question, I think the `Reflect.get` case is interesting. Would you expect `unsafe { Reflect.get(sharedStruct, "foo") }` to work?
IIRC, C#'s unsafe (which is primarily for working directly with pointers) does not require unsafe to interact with pointers via reflection, but C#'s reflection is significantly different from JS's.
00:05
<rbuckton>
If we did require unsafe, then Reflect.get et al would also need an UnsafeCall
00:05
<Mathieu Hofman>
Good point. I would expect `Reflect.get` to throw if not in an unsafe context.
00:06
<rbuckton>
But there would still be no carryover of unsafe { proxyForS.x } through a get trap, and just marking every proxy trap unsafe is dangerous.
00:07
<Mathieu Hofman>
Which now means we need an unsafeCall trap for proxies if we expose this to user land. Ugh
00:08
<shu>
hey man i'm also happy being laissez-faire with data races
00:08
<rbuckton>
We could instead have new Proxy(s, { get(target, key, receiver, unsafe) { return Reflect.get(target, key, receiver, unsafe); } }) and traffic the caller's unsafe-ness around as a parameter.
00:10
<shu>
seems fine
00:10
<Mathieu Hofman>
Seems not, that would effectively allow creating unsafe context without syntax
00:12
<shu>
maybe unsafe will be some unforgeable capability token?
00:12
<shu>
i guess we can't prevent it from being exfiltrated
00:12
<rbuckton>

We either have all of this complexity, or we say:

  • no function coloring
  • Atomics methods are internally unsafe (so Atomics.load(s, "x") doesn't require an unsafe block)
  • Reflect methods are internally unsafe (so Reflect.get(s, "x") doesn't require an unsafe block)
  • The fact that a shared struct field is unsafe is carried through a proxy as we do other invariants in proxies, so you can't transparently make a Proxy "safe" if its fields are unsafe.
00:13
<rbuckton>
For the 4th bullet, that would mean new Proxy(s, { get() { } }).x would throw outside of an unsafe block without ever invoking the get trap
00:13
<shu>
i am definitely coming around to Atomics being internally unsafe, after what i said above
00:13
<shu>
in fact that's basically all Atomics do, access shared memory
00:13
<shu>
so they have to be internally unsafe in a no function coloring world
00:14
<rbuckton>
in fact that's basically all Atomics do, access shared memory
Access shared memory and enforce sequential ordering of memory accesses, which is a coordination mechanism.
00:14
<shu>
yes, fair
00:14
<Mathieu Hofman>
Maybe for atomics, but I'm a lot less comfortable for reflect
00:15
<iain>
Atomics are grandfathered in, and it's not too bad to say "grep for 'atomics' and 'unsafe' to audit"
00:15
<rbuckton>
Otherwise what's the purpose of all of the happens-before and all of the other ordering relations in https://tc39.es/ecma262/#sec-relations-of-candidate-executions
00:15
<iain>
I agree that reflect is a harder case
00:16
<shu>
it could also be that Reflect methods are not internally unsafe, so they just straight up don't work in any context on shared structs
00:16
<shu>
i can live with that
00:16
<rbuckton>
Atomics are grandfathered in, and it's not too bad to say "grep for 'atomics' and 'unsafe' to audit"
I'm not so sure I would characterize Atomics as "grandfathered in", given they are already a complex coordination mechanism.
00:16
<shu>
you then have to add Reflect.unsafeGet and Reflect.unsafeSet that are internally unsafe
00:16
<iain>
What I mean is that if you want to audit potential data races in your code, you have to look at your uses of Atomics, and we can't put that horse back in the barn
00:17
<shu>
no, Atomics can never exhibit data races
00:17
<shu>
only normal races
00:17
<iain>
Sorry, yeah, that's what I meant
00:20
<shu>
i'm off for the rest of the week. good progress and discussion everyone
00:20
<rbuckton>
I think "no function coloring" is a far simpler approach, overall. We shouldn't buy into that complexity unless it is absolutely necessary.
00:21
<rbuckton>
I think it has some interesting benefits, but I don't know that they outweigh the implementation complexity.
00:21
<iain>
I think it is worth preserving flexibility to add it later if it does not significantly conflict with other goals
00:21
<iain>
But I do not want function colouring now
00:23
<rbuckton>
In which case, I would still argue in favor of unsafe function f() {} as meaning something closer to C#'s interpretation than Rust's, in that unsafe in this case is only tagging the declaration as having an unsafe lexical scope, since unsafe tagging readily solves issues with lifting safe entrypoints to unsafe code out of an unsafe {} block.
00:24
<rbuckton>
We already have this problem with private state, I'd like us not to repeat that mistake.
00:31
<rbuckton>
// c.js

// expose #x to other declarations in the same lexical scope
let getX;

export class C {
  #x;
  static {
    getX = c => c.#x;
  }
}

export class FriendOfC {
  method(c) {
    x = getX(c); // privileged access to #x
  }
}

// other.js
import { C, FriendOfC } from "./c.js";

// can't get to C's #x

While it's one of the reasons I proposed static {}, it's still awkward to work with.