00:07
<littledan>

Here’s an idea for the semantic details for unsafe, Reflect, Atomics, and MOP for shared structs:

  • There is an abstract op, GetUnsafe(obj, propKey), which checks whether the obj is a shared struct, if so tries to get the propKey, if it is missing or if it isn’t a shared struct, fall back to Get. Analogously for SetUnsafe.
  • Reflect.getUnsafe/setUnsafe expose these ops
  • inside of an unsafe {} block, all direct property access is interpreted as GetUnsafe/SetUnsafe
  • Get and Set on shared structs are missing their own data properties. Those props don’t show up for any other MOP things either. But the thread-local prototype is present (it isn’t unsafe; a method might call an unsafe thing as an implementation detail though)
  • Atomics are always unsafe (that’s literally the point) so they are just overloaded for shared struct properties regardless of where they come from.
  • if we were doing SAB today, we might also consider this same unsafe restriction, but what’s done is done. This only applies for shared structs.
01:10
<rbuckton>
The property keys need to show up in MOP operations. in and hasOwnProperty and Reflect.has are safe because structs have a fixed layout.
01:12
<rbuckton>
Though [[Get]] and [[Set]] would throw
01:14
<rbuckton>
What do you mean by "Atomics are always unsafe?" my perspective is that Atomics should not need an unsafe block at all
01:22
<littledan>
What do you mean by "Atomics are always unsafe?" my perspective is that Atomics should not need an unsafe block at all
I think we are saying the same thing
01:22
<rbuckton>
OK
01:23
<littledan>
The property keys need to show up in MOP operations. in and hasOwnProperty and Reflect.has are safe because structs have a fixed layout.
Sure, that makes sense. The important thing is that normal MOP operations can’t get at the contents, it’s just this other operation that can
01:23
<rbuckton>
The rest of what you describe sounds like another namespace (like private names) which we absolutely do not want
01:23
<littledan>
The rest of what you describe sounds like another namespace (like private names) which we absolutely do not want
Not sure what you mean. It is still strings (or maybe symbols)
01:24
<littledan>
I am not especially attached to the idea I wrote above, it is just the simplest thing I can imagine. How do you think unsafe blocks should work with respect to the MOP?
01:24
<rbuckton>
It sounded like you were saying that shared struct properties are transparent to MOP operations, which would not be correct
01:25
<littledan>
It sounded like you were saying that shared struct properties are transparent to MOP operations, which would not be correct
Not transparent, just missing
01:25
<littledan>
Maybe that is what you meant
01:25
<rbuckton>
Yes, thats what I meant
01:25
<rbuckton>
they cannot be missing
01:26
<rbuckton>
You cannot have a [[Get]] outside of unsafe return a prototype property if there was a struct field of the same name.
01:26
<littledan>
Can you explain how you think it should work?
01:26
<rbuckton>
They have to treat them like normal properties, except that [[Get]] and [[Set]] throws.
01:26
<littledan>
How?
01:27
<rbuckton>
You override [[Get]] and [[Set]] for shared struct objects.
01:27
<rbuckton>
Those are abstract.
01:28
<littledan>
Will GetOwnPropertyDescriptor throw?
01:28
<rbuckton>
Lets say you have [[Get]], [[Set]], [[UnsafeGet]], and [[UnsafeSet]]. On all objects, [[UnsafeGet]]/[[UnsafeSet]] just forwards on to the ordinary get/set behavior.
01:28
<littledan>
What happens in the unsafe blocks?
01:28
<rbuckton>
But shared structs have a [[Get]] and [[Set]] that throw.
01:28
<rbuckton>
In an unsafe block, get operations use [[UnsafeGet]]/[[UnsafeSet]] instead of [[Get]]/[[Set]]
01:29
<rbuckton>
Even without unsafe we need to do something similar to handle shared memory access for shared struct fields in [[Get]] and [[Set]], so we already expect to pay this cost.
01:31
<rbuckton>
GetOwnPropertyDescriptor would probably throw outside of unsafe, or possibly would return a new descriptor that is { enumerable: ?, writable: ?, configurable: false, shared: true } with no value property.
01:31
<littledan>
OK, so how does Object.getOwnPropertyDescriptor know if it’s in an unsafe block?
01:31
<rbuckton>
But in and Reflect.has et al should work outside of unsafe because for a given reference to a shared struct, it will still have a fixed shape.
01:32
<littledan>
I was trying to avoid functions changing behavior based on their caller
01:33
<littledan>
You cannot have a [[Get]] outside of unsafe return a prototype property if there was a struct field of the same name.
I think this problem can be fixed in my suggestion without making any new MOP ops or anything
01:33
<rbuckton>
We could have gOPD return a new kind of descriptor both in and out of unsafe, and an Reflect.unsafeGetOwnPropertyDescriptor that has the same magic that Reflect.unsafeGet/Reflect.unsafeSet would have (if any).
01:34
<littledan>
Maybe gOPD would throw if you don’t call the unsafe one?
01:35
<rbuckton>
You need MOP operations to be reliable. What happens if I do Object.create(sharedStruct)? Now I have a normal JS object with a shared struct prototype. If I call [[Get]] on the result it should still throw if it tries to read a prototype field outside of unsafe.
01:35
<littledan>
Do we have unsafeDefineProperty?
01:35
<rbuckton>
getOPD shouldn't throw. Nothing causes it to throw today, to my knowledge.
01:35
<rbuckton>
No. You can't call defineProperty on a shared struct, it would fail.
01:35
<rbuckton>
Shared struct instances are sealed.
01:35
<rbuckton>
No new properties, no deleting properties.
01:35
<littledan>
Even if the property descriptor matches what’s already there?
01:36
<littledan>
getOPD shouldn't throw. Nothing causes it to throw today, to my knowledge.
Proxy can
01:36
<rbuckton>
Normal defineProperty would just fail because of the existing integrity checks
01:36
<littledan>
Normal defineProperty would just fail because of the existing integrity checks
I don’t think that’s the case if you define it as what it’s already defined to be, but with a different value
01:36
<rbuckton>
AFAIK, no developers code defensively against gOPD failing.
01:36
<rbuckton>
That's fair
01:37
<rbuckton>
Maybe we do need unsafeDefineProperty. I do want to be able to change writable
01:37
<rbuckton>
But you can't create new properties with it.
01:39
<rbuckton>
Maybe instead of Reflect.unsafeX we have Reflect.unsafe.X which just mirrors Reflect
01:39
<littledan>
I would start simple and omit unsafeGOPD and unsafeDP, letting these always throw on shared struct data props. That might be the only observable difference between the ways we are thinking about this.
01:39
<rbuckton>
(except for deleteProperty since that will never work?)
01:39
<littledan>
Maybe instead of Reflect.unsafeX we have Reflect.unsafe.X which just mirrors Reflect
I am a fan of namespace objects, but I don’t know how much of this we need to fill in
01:40
<rbuckton>
I really would like to make fields non-writable, though I've been thinking we some kind of "init-only" modifier for fields that can only be initialized in the constructor.
01:41
<littledan>
I really would like to make fields non-writable, though I've been thinking we some kind of "init-only" modifier for fields that can only be initialized in the constructor.
Yeah I don’t think nonwritable is a good solution for this. We would need initializer lists. Anyway I imagined shared struct fields would be nonconfigurable
01:41
<rbuckton>
But we probably should have some kind of getOwnPropertyDescriptor support at some point.
01:41
<rbuckton>
Yes, they are non-configurable
01:42
<littledan>
So… no particular use for defineProperty then
01:42
<littledan>
But we probably should have some kind of getOwnPropertyDescriptor support at some point.
Some kind of introspection would be good, but maybe this should be focused on the class level
01:43
<rbuckton>
Even if we don't have gOPD, I want to make sure we can still do { ...sharedStruct } inside of an unsafe block as it could be an efficient way to copy the properties off of the struct while in a lock.
01:44
<littledan>
Even if we don't have gOPD, I want to make sure we can still do { ...sharedStruct } inside of an unsafe block as it could be an efficient way to copy the properties off of the struct while in a lock.
Huh, how do you attach the right cross realm prototype identifier?
01:44
<rbuckton>
you don't? You're not creating a shared struct instance, just a normal object.
01:44
<rbuckton>
Shared struct instances can only be created via a constructor.
01:45
<littledan>
Oic. Yes that should be handled like . Access
01:45
<rbuckton>
{ ...sharedStruct } is "give me a normal object that is a copy of the struct fields"
01:47
<Mathieu Hofman>
I skipped a lot of the discussion, but do shared properties have to appear as data properties, or could they appear as own accessors?
01:48
<Mathieu Hofman>
I guess accessors would be a significant overhead and that engines wouldn't always be able to optimize the same as data props?
01:55
<littledan>
The problem we are trying to solve is how to explain unsafe blocks. I don’t see how accessors help.
01:58
<Mathieu Hofman>
Well accessors means there are no issues with any of the MOP and no special property descriptors
01:58
<rbuckton>
get already has a lexical rule for "use strict". We could just encode [[Unsafe]] on a Reference Record just as we do [[Strict]], and just have the relevant operations check [[Unsafe]] when resolving the reference.
02:00
<rbuckton>
i.e., GetValue checks for [[Strict]] for variable references. We could modify Step 3.d to check for [[Unsafe]] and call baseObj.[[UnsafeGet]] in that case.
02:01
<Mathieu Hofman>
Of course we're just pushing the problem down into a problem of function invocation working differently depending on the context where the call occurs, sometimes nested in the case of Reflect.get calling an "accessors"
02:01
<rbuckton>
Adding an [[UnsafeGet]] slot on objects seems to mesh better with the current spec than an UnsafeGet AO
02:02
<Mathieu Hofman>
It really feels that function coloring actually explains all this much better
02:02
<rbuckton>
If we don't have function coloring, we could just allow you to call the Reflect.unsafeX outside of an unsafe block. Its in the name, so it's already labeled unsafe.
02:03
<littledan>
Well accessors means there are no issues with any of the MOP and no special property descriptors
How are accessors supposed to know whether they are in an unsafe block?
02:05
<Mathieu Hofman>
How are accessors supposed to know whether they are in an unsafe block?
Yes that's the problem. Accessor simply reduce to a single kind of problem: function calls, instead of also dealing with the other meta ops. But it remains a problem that it's hard to explain the behavior without function coloring
02:06
<littledan>
Could you describe how you picture function coloring to work?
02:06
<rbuckton>

e.g., something like this but with proper support for receiver

Reflect.unsafeGet = (obj, key) => {
  if ({}.hasOwnProperty.call(obj, key)) {
    unsafe {
      return obj[key];
    }
  }
  return Reflect.get(obj, key);
}
02:07
<rbuckton>
How are accessors supposed to know whether they are in an unsafe block?
Accessors like get foo() { }? They don't? They're just a function. If you expose a getter/setter on your struct you need to do your due diligence to make it safe to outside callers.
02:09
<rbuckton>
shared struct S {
  #mut = new Atomics.Mutex();
  #x;
  get x() {
    unsafe {
      using lck = new Atomics.UniqueLock(this.#mut);
      return this.#x;
    }
  }
}

It's nasty, but I suppose that's the point?

02:11
<rbuckton>
Although, without function coloring I don't see how accessor x; could ever work. At least, not without doing unsafe accessor x; or accessor x unsafe; or something
02:13
<Mathieu Hofman>
The way I picture function coloring is that every callable now has 2 ops: `[[Call]]` and `[[CallUnsafe]]`. If you are in an unsafe block, it's CallUnsafe that gets executed. For normal functions, CallUnsafe is the same as Call (maybe it's missing and it falls back to Call when missing?). For shared functions, Call throws (can only be called from unsafe blocks). Reflect and other intrinsics can have different Call and CallUnsafe behaviors, that effectively "forward" the unsafe state of the call site.
02:13
<snek>
this example makes me wonder something... should a shared struct even be exposed? in rust for example you'd write your code like struct Public(Mutex<Shared>), rather than struct Shared { mutex: Mutex<()>, ...Shared }
02:14
<rbuckton>
I'll have to follow up on any other discussion on Monday.
02:15
<rbuckton>
That example I gave is a bad one
02:15
<littledan>
The way I picture function coloring is that every callable now has 2 ops: `[[Call]]` and `[[CallUnsafe]]`. If you are in an unsafe block, it's CallUnsafe that gets executed. For normal functions, CallUnsafe is the same as Call (maybe it's missing and it falls back to Call when missing?). For shared functions, Call throws (can only be called from unsafe blocks). Reflect and other intrinsics can have different Call and CallUnsafe behaviors, that effectively "forward" the unsafe state of the call site.
this sounds coherent to me, but it's not what I would call "function coloring", which would apply recursively somehow, like async/await
02:15
<rbuckton>
But yes, we think a shared struct should be exposed. Mutex and shared struct are not strongly tied to each other.
02:17
<rbuckton>
Function coloring does not imply recursive application. Async/await poisoning occurs because you are taking an inherently sequential, synchronous operation and want to turn it into a sequential asynchronous operation.
02:17
<Mathieu Hofman>
this sounds coherent to me, but it's not what I would call "function coloring", which would apply recursively somehow, like async/await
Right, technically you can have an CallUnsafe implementation that is not itself an unsafe scope
02:17
<rbuckton>
Async/await has function coloring (of a sort), but function coloring is not async/await.
02:18
<snek>
no i don't mean you should have to use mutex specifically, that's just the example here.
02:18
<littledan>
(I'm not criticizing the approach, it's just drastically different from what I expected when people started using the term "function coloring")
02:19
<rbuckton>
no i don't mean you should have to use mutex specifically, that's just the example here.
you can organize your code however you want. My use cases have entire object graphs of shared objects with any coordination being through lock-free concurrent collections.
02:20
<Mathieu Hofman>
(I'm not criticizing the approach, it's just drastically different from what I expected when people started using the term "function coloring")
It's possible I also misunderstood what people had in mind, but that is what I understood could work
02:22
<rbuckton>
I was never concerned about function coloring, just that we didn't repeat async/await poisoning by essentially requiring your entire application to be inside of an unsafe {} block to use the feature.
02:22
<Mathieu Hofman>
I think it would even be possible to make proxies work that way. As well as let user land do the same as intrinsics by having functions that have dual safe and unsafe behaviors
02:23
<rbuckton>
keeping unsafe localized to just the code that is actually unsafe is important.
02:24
<rbuckton>
Having functions that are aware of the context with which they are invoked is nothing new. unsafe is more like this than async/await, to be honest. async functions don't care how you call them and its up to the callers to determine if they want to use await or .then.
02:24
<Mathieu Hofman>
I was never concerned about function coloring, just that we didn't repeat async/await poisoning by essentially requiring your entire application to be inside of an unsafe {} block to use the feature.
Yeah I think that's accomplished by letting you start an unsafe block without modifying the signature of the surrounding function
02:24
<rbuckton>
Having an operation that throws outside of unsafe is more like having a function that throws if you give it the wrong this.
02:26
<rbuckton>
From a spec perspective, we just have to carry along this extra bit of information that indicates whether you were inside or outside of an unsafe block before you get/set.
02:27
<rbuckton>
Aside from whatever we decide for Reflect, we could just ship with unsafe {} and add "function coloring" later if needs be.
02:29
<rbuckton>
For something like unsafe function f() {} I was less concerned with "function coloring" and more about improving the DX by moving the unsafe out of the block to cover the contents of the whole function (including parameter lists). I think the fact I proposed it as a prefix keyword led to the "function coloring" implication of unsafe functions in Rust, that the function itself is somehow unsafe. But it could just as easily have been function f() unsafe { } (and is an alternative I mentioned in the related issue on the repo).
02:30
<rbuckton>
I'm just not a fan of the C++ namespace nesting style. It looks terrible and there's no reason we should repeat that approach.
02:31
<snek>
what if you want a function that should be unsafe to call, unsafe on the declaration referring to the body seems inverted to the expectation of someone using that function.
02:32
<rbuckton>
what if you want a function that should be unsafe to call, unsafe on the declaration referring to the body seems inverted to the expectation of someone using that function.
Then we reserve the prefix position for that, where unsafe <x> ... means "x is unsafe and does unsafe things" while <x> unsafe ... means "x is safe, but does unsafe things".
02:35
<rbuckton>
i.e., function f() unsafe {} is just shorthand for function f() { unsafe { } }. You use that for functions in your API that are at the safe/unsafe boundary. unsafe function f() {}, if we added it, would only be intended to be used for functions inside of your library/app that don't perform any locking as they expect to be called from code that has already done any necessary coordination.
02:36
<snek>
that sounds reasonable
02:36
<snek>
i like composing with block syntax everywhere
02:37
<rbuckton>
unsafe should be as narrow as is reasonable, while being as broad as is useful. I like the idea of being able to write shared struct S unsafe {} and have the whole body be unsafe, but also having shared struct S { foo() unsafe { } } when I want to limit exposure at the edges of a public API.
02:40
<rbuckton>
for example, I might have a shared struct ConcurrentDeque<T> { ... } whose public methods are safe to use and whose contents are private and encapsulated. But I might also want to have a shared struct RingBuffer<T> unsafe { ... } because the whole body will contain unsafe code and the struct won't be exposed outside of my API.
02:42
<rbuckton>
We can defer "function coloring" 'til later. For example, we could add Reflect.unsafeGet now, which internally applies unsafe and thus can be used outside of an unsafe {} block, and have Reflect.get always throw on shared struct fields. If we add "function coloring" later we could possibly modify Reflect.get to have some way to know. Maybe even add a function.unsafe metaproperty that lets you know if you were called from an unsafe context (which better explains a Reflect.get that works conditionally based on invocation context)
02:47
<rbuckton>

e.g., evolve in steps:

  1. unsafe {} . Reflect.unsafeX methods where necessary that can be used from normal code since they're labeled "unsafe".
  2. postfix-unsafe keyword for block declaration bodies to improve DX.
  3. function.unsafe metaproperty so you can explicitly check whether you're being called from unsafe code. Modify Reflect.X functions to conditionally work inside of unsafe using the same context.
  4. prefix-unsafe keyword for functions/methods that essentially check function.unsafe for you and whose contents are implicitly unsafe.
02:52
<snek>
prefix should probably not make the body unsafe. rust is in the process of undoing that right now 😄
02:57
<rbuckton>
Why would it not? What would be the point otherwise?
02:58
<rbuckton>
I definitely don't want to have to write unsafe function f() unsafe {}, that's repetitive and redundant and likely to confuse developers.
03:00
<snek>
it prevents you from scoping unsafe code within the function
03:03
<snek>
i feel like unsafe as a concept is large enough to be its own proposal 😄
03:04
<rbuckton>
If you are limiting the unsafe scope in the function, why would you declare the function unsafe?
03:05
<rbuckton>
(on phone and autocorrect failed me)
03:06
<snek>
perhaps the function itself does not perform locking, and relies on the caller for that
03:07
<rbuckton>
If we decided to add a function.unsafe metaproperty, then we could handle the case of limiting scope while still "coloring the function"
03:09
<snek>
i don't think function color is actually a problem here, it just exists to control how you think about your program. you can always write a safe wrapper around any function regardless of what color it is.
03:09
<rbuckton>
Ooh, better idea in.unsafe 🤔
03:11
<snek>
i feel like the reason for unsafe existing and making unsafe a magic property you can control flow on are kind of at odds with each other
03:11
<rbuckton>
Well, maybe not better.
03:13
<rbuckton>
I think having unsafe function f() only color the function but not mark the body as unsafe would be terribly confusing.
03:14
<snek>
i think it makes a lot of sense, unless you require that every statement in an unsafe function is itself unsafe
03:14
<rbuckton>
But if we wanted to have Reflect.get only work on shared structs inside of unsafe, that is more dependent on a function.unsafe-like control flow operation than function coloring.
03:15
<rbuckton>
i think it makes a lot of sense, unless you require that every statement in an unsafe function is itself unsafe
That doesn't seem feasible or sensible.
03:16
<snek>
yeah i mean that's sort of my point. the implementation of the function is probably a mix of safe and unsafe, and you're likely interested in calling attention to certain parts of it without allowing more unsafe code to slip in unnoticed.
03:17
<rbuckton>
I absolutely don't want people to have to write dozens of unsafe {} blocks in a single function if they don't need to.
03:17
<snek>
and wrt reflect.get... if a struct wanted to participate in some existing code that uses reflect.get somewhere internally, it would have to expose a getter that enforces that access of that property is safe, so that the reflect.get is not unsafe. having an unsafe somewhere above it does not enforce the constraint that the reflect.get was written with reasonable intent.
03:18
<rbuckton>
They can if they want to, obviously, but that shouldn't be a requirement.
03:19
<snek>

they don't need to.

what does need to mean? if the point of unsafe existing is to call your attention to certain code, i'd say the "need" is making each occurrence as targeted as possible.

03:19
<rbuckton>
If we had the ability to mark a shared struct property as writable: false, then it could potentially become safe to read outside of an unsafe {} block since it can no longer change.
03:19
<snek>
it could also just be readable from [[Get]] in that case
03:22
<rbuckton>

they don't need to.

what does need to mean? if the point of unsafe existing is to call your attention to certain code, i'd say the "need" is making each occurrence as targeted as possible.

I think I was taking your "only write unsafe statements in unsafe {} blocks" to the extreme. There are a lot of JS operations that are "safe" and juggling unsafe {} blocks to work around that would be a nightmare. The reality is more that unsafe {} should be scoped to the level that you, as a developer, need it to be.
03:23
<rbuckton>
But having unsafe function f() {} not making the body unsafe would break with existing JS paradigms like async and function*.
03:24
<snek>
wdym break
03:24
<rbuckton>
break with, as in differ from in a way that could be confusing.
03:24
<rbuckton>
break away from, deviate
03:25
<rbuckton>
I'd like to argue for the principle of least surprise here. If I say a function is unsafe, then I expect it to be unsafe.
03:26
<snek>
oh i see. i don't think i've seen any evidence that similar constructs are confusing in other languages. unsafe/extern/etc in rust and c++ and c and on and on are good prior art there
03:26
<snek>
i lack hard data one way or another though
03:26
<rbuckton>
If unsafe only colors the function and does not apply to the body, then it differs from async or * in that regard.
03:27
<snek>
its also not a dangerous confusion. if you expect the body to be unsafe and it isn't, you haven't done anything unsafe accidentally
03:27
<rbuckton>
If we wanted to give a way to just color a function without marking the lexical scope, we could offer up a decorator for that purpose.
03:29
<rbuckton>
But to back up for a bit, If we wanted Reflect.get to have different behavior inside or outside of unsafe, or for proxies to be able to convey whether their hooks are evaluated in unsafe code, that is not actually something that is solved by function coloring.
03:30
<rbuckton>
Function coloring seems more of a binary state. You are either safe to call, or you are not. Conditional behavior based on context is different.
03:31
<rbuckton>
function.unsafe would explain that and would be accessible to proxies.
03:31
<snek>
the conditional behavior makes me feel uncomfortable
03:31
<snek>
also why would proxies need it, isn't this already disambiguated to them via [[Get]] vs [[GetUnsafe]]?
03:32
<rbuckton>
The question is more, do we need a separate getUnsafe hook?
03:32
<snek>
if we represent this as a new mop operation then i think that is sort of implied right
03:33
<rbuckton>
The [[Get]] vs [[GetUnsafe]] is more of a design we were initially discussing for implementations. It could also just be an argument passed to the MOP operation as far as the spec is concerned.
03:33
<snek>
then it would be an argument passed to the get method of the proxy
03:35
<rbuckton>
That's also an option, but then it would be something only a Proxy could observe but couldn't be observed from user code. Then again, so would an unsafeGet hook
03:36
<snek>
what does "observed from user code" mean? you already can't observe what operator something used to reach your function, you have to trap it with a proxy.
03:36
<rbuckton>
Having a set of get/unsafeGet, set/unsafeSet, apply/unsafeApply, etc. hooks is just as conditional as function.unsafe
03:40
<snek>
yes... function calls are a form of control flow. that's not what i meant earlier though...
03:40
<rbuckton>
We have new.target to differentiate between Reflect.apply/f() and Reflect.construct/new
03:43
<snek>
in class constructors it does not represent that. and using it in normal functions is not a common pattern anymore.
03:44
<rbuckton>
I think we're getting into the weeds with this discussion. I can understand the perspective that you might want to color the function without making the body unsafe, I'm just not sure I agree with it. There are many C++ idioms I'd rather not repeat in JS, and as much as I want to increase the flexibility of the language, I prefer to find ways that are in keeping with the current design of the language where possible.
03:45
<rbuckton>
in class constructors it does not represent that. and using it in normal functions is not a common pattern anymore.
That is a product of class constructors having an intentionally broken [[Call]], not a product of the design of new.target.
03:46
<rbuckton>
In fact, new.target is a way that decorators could be used to easily define "callable classes", which had their own proposal at one point.
03:47
<rbuckton>
In any case, it does exist and is a precedent.
03:50
<snek>
new.target exists therefore in.unsafe must also exist?
03:50
<rbuckton>
unsafe function f() unsafe {} (or unsafe function f() { unsafe { } }) is aesthetically unpleasant and overly pedantic.
03:51
<rbuckton>
Not in.unsafe, after I said that I realized that's pretty useless as you don't need to query if you're in an unsafe block, that's established lexically. function.unsafe is clearer as its tied to the invocation of the function/getter/constructor/etc., not the lexical context.
03:51
<snek>
replace my message with function.unsafe then, i don't care what its called
03:51
<rbuckton>
Not must, but it sets a precedent we could/should follow if we introduce something similar.
03:52
<rbuckton>
apply/construct are dual hooks that indicate whether a function was called without or with new, and can be observed in the function itself via new.target.
03:53
<snek>
should we add function.async too since it might've been awaited? i don't feel like this argument is self-consistent or based in any real goal
03:53
<rbuckton>
Similarly, get/unsafeGet are dual hooks that indicate whether a field or accessor was accessed outside or inside an unsafe block, an can be observed within the accessor via function.unsafe. There are direct parallels
03:57
<rbuckton>
No because await f() consists of two distinct operations (call and then await). In new f() and unsafe { f() }, the context is intrinsically linked to the invocation.
03:59
<snek>
called in an async function then, it doesn't really matter. my point is that we can expose any random detail of execution as an inspectable property, but the actual thing to discuss is whether doing so is meaningful, not whether its possible.
04:00
<rbuckton>
The point of function.unsafe is it explains a world where Reflect.get has conditional behavior based on unsafe {}, and acts as a carve-out for the pedantic case of "I want a function that acts like its colored as unsafe but doesn't have an unsafe body"
04:01
<rbuckton>
That doesn't have to be the answer, but I'm not a fan of repetition for the common case, especially when it diverges from other stylistic norms in JS like async and *
04:02
<rbuckton>
Maybe we have an unsafe function f() safe { } for the pedantic case
04:02
<rbuckton>
not that I really want two opposing keywords
04:02
<snek>
it certainly does explain that, but what i was questioning above was not how to explain such behavior. it was whether such behavior should exist.
04:11
<rbuckton>

If we have split hooks, then conditional behavior exists so long as you use a Proxy, but if you need to use a Proxy just to only apply function coloring, that means you probably cannot use such a function in performance critical code. That's not a dealbreaker, as there are other ways to achieve "colored-unsafe-but-not-unsafe", such as

unsafe function f() { return g(); }
function g() {}

or

@(t => unsafe function() { return t.apply(this, arguments); })
function f() {
}

or

@MarkUnsafe function f() {}

or

const f = markUnsafe(function() {});
04:12
<rbuckton>
So we don't necessarily need function.unsafe, it just happens to check a number of boxes in the design.
04:13
<snek>
what are the boxes that it checks
04:16
<rbuckton>
  • Explains the behavior of a Reflect.get et al that differ based on whether they are called inside of unsafe {}
  • Allows user code to also emulate conditional behavior of Reflect.get, et al, performantly (i.e., not through a Proxy)
  • Provides an escape hatch for "colored-unsafe-but-not-unsafe" via function f() { if (!function.unsafe) throw ...; }
  • Thematically aligned with existing concepts in JS (in this case, new.target)
04:17
<snek>
sorry please believe me that i'm trying to engage in good faith here. but i feel like we just went in a circle. i asked why reflect.get should have magic behavior instead of requiring the shared struct to expose a safe property and you responded with "this enables reflect.get to have magic behavior" which doesn't answer my question.
04:20
<rbuckton>
I thought this was about whether unsafe function f() {} marks the block unsafe? I was using the function.unsafe metaproperty as an escape hatch for anyone who needs a pedantic "colored-unsafe-but-not-unsafe" function, with examples of how such a metaproperty would explain various behaviors we've been discussing.
04:21
<rbuckton>
If we decide any of the bullets above aren't a goal, that obviously weakens function.unsafe. I'm also not arguing as a steadfast supporter of such a metaproperty, I honestly don't have a strong opinion on it.
04:21
<snek>
ah. i apologize for the confusion.
04:22
<rbuckton>
My position is that unsafe function f() { unsafe {} } is a terrible design and we shouldn't need to do that.
04:24
<snek>
my experience from other languages is that it would not be that comically repetitive in practice. but perhaps we should write up some examples
04:24
<rbuckton>
The Rust language has very specific design goals in mind, and this kind of pedantry is part and parcel of that approach.
04:28
<rbuckton>
I've written several thousand lines of TypeScript code using the dev trial version of shared structs, and a lot of my concern comes from where I expect the boundaries would be if I had to litter that code with unsafe {}. I also strongly prefer language designs that cut down on excess ceremony and have consistent syntax and mechanics.
04:29
<rbuckton>
unsafe {} is already a compromise, I'd like to make it as unobtrusive as is feasible.
04:31
<rbuckton>
In general, I'd prefer no function coloring at all. Have Reflect.get throw for shared struct fields and Reflect.unsafeGet work in or out of an unsafe block, or even just have Reflect.get always work on unsafe things since you're already reaching for something more complicated than a.b.
04:31
<rbuckton>
My initial proposal for unsafe function f() {} wasn't intended to imply coloring at all.
04:32
<snek>
i'm also fine with unsafe as a concept not existing. but if it must exist then i want us to at least get something with a reasonable usage model out of it 😄
04:40
<rbuckton>
So far as I understand it, it (or something much like it) must exist to achieve consensus. The less we have to go over and above that the better, but whatever we choose to do with it beyond that, we must endeavor to align it with the rest of the JS language and follow from the same design choices and principles we've followed in the past. I don't want to add function coloring for function coloring's sake. I don't want it to become a repeat of async/await poisoning. I don't want it to have so much scope creep that the proposal never advances purely because we've tacked too many things on. If we have ways to leave space to incrementally adopt other functionality in the future, that's fine, but I've seen too many proposals take on too much and stagnate.
04:41
<snek>
well at the very least it won't be a repeat of async/await, because it is not viral, thank god
04:42
<rbuckton>
If we have strong motivations and clear rationale for why we need function coloring, then by all means lets find a solution for that. But if we can find an alternative that doesn't require it, I'm going to favor the alternative.
04:46
<rbuckton>
Don't get me wrong, I absolutely love async/await. It's the fact that you can't choose to synchronously wait for a promise to complete when it would be appropriate to do so that is the problem, which is something you can do in numerous languages with a more robust model for shared memory multi-threading.