| 00:07 | <littledan> | Here’s an idea for the semantic details for unsafe, Reflect, Atomics, and MOP 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 |
| 01:22 | <rbuckton> | OK |
| 01:23 | <littledan> | The property keys need to show up in MOP operations. |
| 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 |
| 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 |
| 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 |
| 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. |
| 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 |
| 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 |
| 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. |
| 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 |
| 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 |
| 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 |
| 02:05 | <Mathieu Hofman> | How are accessors supposed to know whether they are in an unsafe block? |
| 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
|
| 02:07 | <rbuckton> | How are accessors supposed to know whether they are in an unsafe block? 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> |
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. |
| 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 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. |
| 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") |
| 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 |
| 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 <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:
|
| 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 |
| 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> |
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> |
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. [[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
or
or
or
|
| 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> |
|
| 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. |