Skip to content

Conversation

dminor
Copy link
Collaborator

@dminor dminor commented Oct 3, 2025

This is an attempt to reflect web reality and allow an implementation-defined choice of whether to use a property (like SpiderMonkey and JSC) or accessors (like V8). Allowing an implementation-defined choice came up in a couple of conversations, including #1 (comment).

The accessor sets a private property, which is what V8 currently does. If I recall correctly, the objection to a private property came from JSC, but if we go with this direction, JSC could continue to use a data property, so hopefully this won't be problematic. If it turns out to be objectionable, we could instead use closures, as was suggested by bakkot, see #1 (comment).

@dminor
Copy link
Collaborator Author

dminor commented Oct 3, 2025

For reference, the V8 implementation appears to be here.

spec.emu Outdated
1. If ? IsExtensible( _error_ ) is *false*, throw a *TypeError* exception.
1. Let _name_ be an implementation-defined name.
1. ! PrivateFieldAdd( _error_, _name_, _string_).
1. Perform ! OrdinaryDefineOwnProperty(_error_, *"stack"*, PropertyDescriptor { [[Get]]: ? PrivateGet( _error_, _name_ ), [[Set]]: ? SetterThatIgnoresPrototypeProperties(_error_, OrdinaryObjectCreate(*null*), *"stack"*, _string_)., [[Enumerable]]: false, [[Configurable]]: true }).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The values of [[Get]] and [[Set]] must be function objects. I think what you're trying to do here is define abstract closures each with a single step and create built-in functions from those. You can use CreateBuiltinFunction for this, following the example of MakeArgGetter.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since all of this is actually not observable do we really need to spec it at that level? Here is my try:

let _getter_ be a function object such that _getter_.[[Call]](_error_) returns _string_ and
let _stack_ be PropertyDescriptor { [[Get]]: ? _getter_ }

Copy link

@o- o- Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw. if I understand correctly currently that setter (as a function) would override itself with the stacktrace in _string_. Did you intended it to override itself with the value passed to the setter.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need to spec it at that level?

Yes, I think we do. Saying it is any function object with that [[Call]] behaviour doesn't go into how it's created, what realm it's in, what its [[Prototype]] is, etc., which are observable characteristics that should be specified.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that setter (as a function) would override itself with the stacktrace in _string_. Did you intended it to override itself with the value passed to the setter.

Yes that is a requirement to avoid the getter / setter pair creating a covert communication channel on ordinary objects.

spec.emu Outdated
1. Or:
1. If ? IsExtensible( _error_ ) is *false*, throw a *TypeError* exception.
1. Let _name_ be an implementation-defined name.
1. ! PrivateFieldAdd( _error_, _name_, _string_).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on the above chosen name, PrivateFieldAdd may throw (because the field exists). I think you need to constrain the name further somehow.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, I don't see any reason to use a private field here since there's no other consumers of it. I like @bakkot's suggestion of just closing over the data in both ACs.

spec.emu Outdated
1. Else,
1. Let _string_ be an implementation-defined string that represents the current stack trace.
1. Perform ? SetterThatIgnoresPrototypeProperties(_error_, OrdinaryObjectCreate(*null*), *"stack"*, _string_).
1. Perform an implementation-defined choice of either:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an editorial nit, but I would prefer a step more like

Let _strategy_ be an implementation-defined choice of either ~data-property~ or ~getter~.

and then typical constructions following that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I don't know if you want to permit this choice to be made independently every time Error.captureStackTrace is called (as it would with both what you've written and my suggestion) or if you want that choice to be consistent within some unit of execution. For example, it may be a field of the Agent.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how having it a field of the agent change anything, unless that field is restricted from changing. Such restriction could similarly apply to algorithmic steps, no? I thought we had some similar constraints elsewhere.

Copy link
Member

@michaelficarra michaelficarra Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do this already for endianness as a field of the Agent Record:

Once the value has been observed it cannot change.

The entire Agent Record should probably have the same constraint, but that's a separate conversation. If we were to do the same for algorithm steps, you'd have to be explicit about the unit of execution for which it's meant to be consistent. Putting it on the Agent (or some other value like an Environment Record) makes it clear what that unit of execution is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

field on agent (for scope) that cannot change works for me

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't make unobservable constraints like those. I think the widest context we could constrain is an agent cluster.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand. What I'm asking is exactly about about what's observable in a realm, and making sure it remains at a minimum deterministic.

I would prefer if the choice was made solely at realm or agent creation time, but I could be convinced that the choice could be made dependent on the target (I can't imagine a use case so I'd prefer not to). I don't want it to be a random choice every invocation.

Copy link
Member

@michaelficarra michaelficarra Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was responding to your edit:

if the implementation is going to produce an accessor under some circumstances, I want it to always produce an accessor for those circumstances

We can't say "an implementation must always do it the same way", but we can say that it would never be observable that an implementation made different choices because those programs are being executed in separate agent clusters and thus can never communicate.

I don't want it to be a random choice every invocation.

Okay then we should probably put the field on the Agent Record like I suggested and make a guarantee analogous to this existing one for [[LittleEndian]]:

All agents within a cluster must have the same value for the [[LittleEndian]] field in their respective Agent Records.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry my edit was meant to relax my requirement that within an agent a different choice could be made as long as it's deterministic. Really not my preference so let's not relax this unnecessarily

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion of using an Agent Record field.

@mhofman
Copy link
Member

mhofman commented Oct 3, 2025

The accessor sets a private property, which is what V8 currently does. If I recall correctly, the objection to a private property came from JSC,

To clarify, the change as currently proposed in this PR does not fully reflect what v8 is doing, which is a good thing. We are one of the objectors to private properties as currently implemented by v8. To summarize our objection to accessors is that there cannot be getter/setter pairs that work to modify and read result of modifications on different objects. That means that either:

  • the accessors can be shared and the getter work against a private field, as long as the setter does not modify said private field (it can define a data property on the target)
  • the accessors are unique per object and in effect form a closure of the private stack data (as suggested by bakkot). The setter may update that closure data (personally prefer not to), but could also be a shared setter that defines a data property.

Please note these options have differences observable to the program so I don't think we should allow a choice of them to the implementation (this PR seem to currently prescribes the first one, with what looks like a shared getter).

@ljharb
Copy link
Member

ljharb commented Oct 3, 2025

This doesn't seem like a good approach - if it can be either one, then we have an opportunity to force one choice or the other, and we should always be minimizing implementation differences.

@mhofman
Copy link
Member

mhofman commented Oct 3, 2025

I think this is a pragmatic way to address potential future spec changes regarding error stacks, as discussed in plenary last time. If we ever want to standardize something like prepareStackTrace, we will need the ability to make these stack properties trigger user code, which isn't possible for an ordinary data property. I also understand we do not want to impose the complexity of accessors and slots on all implementation today.

@ljharb
Copy link
Member

ljharb commented Oct 3, 2025

imo it would be better to have a willful violation in a single browser than to allow implementations this much latitude.

@o-
Copy link

o- commented Oct 6, 2025

To clarify, the change as currently proposed in this PR does not fully reflect what v8 is doing

Can you elaborate. I didn't mange to spot the divergence? (Or are you referring to prepareStackTrace).

Update: ah our setter writes to the private field. is that what you are referring to?

spec.emu Outdated
1. Let _string_ be an implementation-defined string that represents the current stack trace.
1. Perform ? SetterThatIgnoresPrototypeProperties(_error_, OrdinaryObjectCreate(*null*), *"stack"*, _string_).
1. Perform an implementation-defined choice of either:
1. Perform ? SetterThatIgnoresPrototypeProperties(_error_, OrdinaryObjectCreate(*null*), *"stack"*, _string_)..
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To bring the two cases closer together it would be nice if both cases go through the same definition primitive. E.g., we could have an implementation specific PropertyDescriptor ([[Get]] vs. [[Value]]) which is then installed in the same way (e.g., OrdinaryDefineOwnProperty) in both cases.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer all of the things that share the behaviour described in SetterThatIgnoresPrototypeProperties go through that AO. It's a very specific exception to how we usually do things in the language and it should both have fully consistent normative requirements and be discoverable. Reusing that AO helps with both goals.

@dminor
Copy link
Collaborator Author

dminor commented Oct 6, 2025

To clarify, the change as currently proposed in this PR does not fully reflect what v8 is doing

Can you elaborate. I didn't mange to spot the divergence? (Or are you referring to prepareStackTrace).

Update: ah our setter writes to the private field. is that what you are referring to?

Yes, I was attempting to follow Mathieu's constraint that the setter not write to the private field, in this comment #1 (comment), to avoid having to specify closures like bakkot suggested. This seemed like the smallest delta from the current behaviour that would be acceptable.

I'm ok with specifying closures as long as V8 is, as they'd have to change their implementation. It currently uses the same getter and setter across instances.

@dminor
Copy link
Collaborator Author

dminor commented Oct 6, 2025

imo it would be better to have a willful violation in a single browser than to allow implementations this much latitude.

I'm sympathetic to this point of view, my initial idea was that we should just specify one behaviour. That said, I think this is pragmatic, and that it's better for web developers to have an implementation-defined choice of two behaviours that are fully specified, rather than having an implementation continue to ship unspecified behaviour, or everyone continuing to ship unspecified behaviours.

@mhofman
Copy link
Member

mhofman commented Oct 6, 2025

Can you elaborate. I didn't mange to spot the divergence? (Or are you referring to prepareStackTrace).

Update: ah our setter writes to the private field. is that what you are referring to?

Very much, yes. Please see https://issues.chromium.org/issues/40279506 and #1 for why this is a problem. FWIW we would like the own .stack accessor for errors in v8 to also change

@dminor
Copy link
Collaborator Author

dminor commented Oct 7, 2025

I've updated this with the suggestions to use an agent record field to control behaviour and closures for the getter and setter.

spec.emu Outdated
</thead>
<tr>
<td>[[UseErrorCaptureStackTraceDataProperty]]</td>
<td>a Boolean</td>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: since this isn't a yes/no, it's an either/or, we would typically use a 2-state enum for this. Something like [[ErrorCaptureStackTraceStrategy]] with type "~data-property~ or ~accessor~".

spec.emu Outdated
1. Perform ? SetterThatIgnoresPrototypeProperties(_error_, OrdinaryObjectCreate(*null*), *"stack"*, _string_)..
1. Else,
1. If ? IsExtensible( _error_ ) is *false*, throw a *TypeError* exception.
1. Let getterClosure be a new Abstract Closure with no parameters that captures _string_ and performs the following steps when called:
Copy link
Member

@michaelficarra michaelficarra Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. Let getterClosure be a new Abstract Closure with no parameters that captures _string_ and performs the following steps when called:
1. Let _getterClosure_ be a new Abstract Closure with no parameters that captures _string_ and performs the following steps when called:

Similarly for all the bindings below.

spec.emu Outdated
1. If _useErrorCaptureStackTraceDataProperty_ is *true*, then
1. Perform ? SetterThatIgnoresPrototypeProperties(_error_, OrdinaryObjectCreate(*null*), *"stack"*, _string_)..
1. Else,
1. If ? IsExtensible( _error_ ) is *false*, throw a *TypeError* exception.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting nit:

Suggested change
1. If ? IsExtensible( _error_ ) is *false*, throw a *TypeError* exception.
1. If ? IsExtensible(_error_) is *false*, throw a *TypeError* exception.

spec.emu Outdated
1. Return NormalCompletion(_string_).
1. Let getter be CreateBuiltinFunction(getterClosure, 0, "", « »).
1. Let setterClosure be a new Abstract Closure with parameters (_value_) that captures _error_ and performs the following steps when called:
1. Perform ! SetterThatIgnoresPrototypeProperties(_error_, OrdinaryObjectCreate(*null*), *"stack"*, _value_).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. Perform ! SetterThatIgnoresPrototypeProperties(_error_, OrdinaryObjectCreate(*null*), *"stack"*, _value_).
1. Perform ? SetterThatIgnoresPrototypeProperties(_error_, OrdinaryObjectCreate(*null*), *"stack"*, _value_).

This AO can fail if the setter is called after the "stack" property is deleted, for example.

spec.emu Outdated
1. Return NormalCompletion(_string_).
1. Let getter be CreateBuiltinFunction(getterClosure, 0, "", « »).
1. Let setterClosure be a new Abstract Closure with parameters (_value_) that captures _error_ and performs the following steps when called:
1. Perform ! SetterThatIgnoresPrototypeProperties(_error_, OrdinaryObjectCreate(*null*), *"stack"*, _value_).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. Perform ! SetterThatIgnoresPrototypeProperties(_error_, OrdinaryObjectCreate(*null*), *"stack"*, _value_).
1. Perform ! SetterThatIgnoresPrototypeProperties(_error_, *null*, *"stack"*, _value_).

This is just used for a SameValue test against _error_, so you just need to pass anything else.

spec.emu Outdated
1. Return NormalCompletion(_string_).
1. Let getter be CreateBuiltinFunction(getterClosure, 0, "", « »).
1. Let setterClosure be a new Abstract Closure with parameters (_value_) that captures _error_ and performs the following steps when called:
1. Perform ! SetterThatIgnoresPrototypeProperties(_error_, OrdinaryObjectCreate(*null*), *"stack"*, _value_).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You still need to return something here.

Suggested change
1. Perform ! SetterThatIgnoresPrototypeProperties(_error_, OrdinaryObjectCreate(*null*), *"stack"*, _value_).
1. Perform ! SetterThatIgnoresPrototypeProperties(_error_, OrdinaryObjectCreate(*null*), *"stack"*, _value_).
2. Return NormalCompletion(*undefined*).

spec.emu Outdated
</thead>
<tr>
<td>[[ErrorCaptureStackTraceStrategy]]</td>
<td>DATAPROPERTY or ACCESSOR</td>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The syntax for spec enums is ~ on either side. Convention is to use lower-kebab-case.

Suggested change
<td>DATAPROPERTY or ACCESSOR</td>
<td>~data-property~ or ~accessor~</td>

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, that is what you suggested originally. I was thrown off by https://tc39.es/ecma262/#sec-enum-specification-type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep they get rendered in all caps but that's not how they look in the markup.

spec.emu Outdated
</table>
</emu-table>

<p>Once the values of [[Signifier]], [[IsLockFree1]], [[IsLockFree2]], and [[UseErrorCaptureStackTraceDataProperty]] have been observed by any agent in the agent cluster they cannot change.</p>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<p>Once the values of [[Signifier]], [[IsLockFree1]], [[IsLockFree2]], and [[UseErrorCaptureStackTraceDataProperty]] have been observed by any agent in the agent cluster they cannot change.</p>
<p>Once the values of [[Signifier]], [[IsLockFree1]], [[IsLockFree2]], and [[ErrorCaptureStackTraceStrategy]] have been observed by any agent in the agent cluster they cannot change.</p>

Need to update this field name here and below.

Copy link
Member

@michaelficarra michaelficarra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

editorially LGTM otherwise

@dminor
Copy link
Collaborator Author

dminor commented Oct 8, 2025

editorially LGTM otherwise

Thank you for all the help with this :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants