Repository navigation
Conversation
|
Preview build of 4a50303: |
… stack trace An error made while a class field is initialized was reported at the constructor that ran the initializer, or at the `class` keyword for a static field. The function that runs the initializers was ImplementationVisibility::Private, and every stack walk leaves such a frame out. Under USE(BUN_JSC_ADDITIONS): - The function that runs instance field initializers is Public and is named <instance_members_initializer>. The function that runs the static elements of a class is Public and is named <static_initializer>. V8 lists the same two names. The names are in CommonIdentifiers. - A static block is a function of its own, and it takes the static name too. When every static element of a class is a block, the function that calls the blocks stays hidden, so such a block lists one frame. - A field records expression info in front of its initializer and in front of the define of a named field. An operation that records no position of its own is then reported at its field, not at the field in front of it, and so is a define that throws. Supersedes #435.
robobun
force-pushed
the
robobun/2a21e0d6/class-field-initializer-frames
branch
from
November 8, 2026 07:35
e5a6d60 to
4a50303
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Do not merge alone. Merge it together with the Bun PR that pins it (see Notes).
Problem
at new W (unknown:1:11)and(near '...(function () { })...')forclass W { port = cfg.port }. A static field is reported at theclasskeyword.emitNewClassFieldInitializerFunction(bytecompiler/BytecodeGenerator.cpp:3828) makes the function that runs the initializersImplementationVisibility::Private. Every stack walk skips it.Fix
USE(BUN_JSC_ADDITIONS)that function isPublic, named<instance_members_initializer>or<static_initializer>as in V8.JSTests/stress/bun-class-field-initializer-stack-frames.js(fails before),JSTests/controlFlowProfiler,JSTests/stress.Background
new, and plain fields stay wrong.Downsides
Error.stackTraceLimitcounts it. Through a static block of a class with static fields it has two<static_initializer>frames. V8 lists one.new: 0. A class with fields costs 500 to 700 more instructions when first evaluated.new Error()in an initializer costs 87 more, 4,067 more with.stack.Error.prepareStackTrace. Static blocks already behave so ([JSC] An error keeps a record per frame of a stack trace that a collection formats #873).Notes
Order of merges. Bun's ahead-of-time compiler hides the same frame in compiled code (
src/aot/lower/class/fields.rs), and compiled programs are compared with plain Bun, so the Bun half changes both. This stays a draft until the Bun half is green on the preview build of this branch. Then: merge this, move Bun'sWEBKIT_VERSIONto the merged commit in that PR, and merge it as the next pin change. The branch is cut from the commit Bun pins (5a61071), so the preview carries this commit only. oven-sh/bun#48120 moves the pin again. I rebase when it lands.Report. Bun 1.4.3 keeps a decorated field a field of the class (oven-sh/bun#40833), so decorated fields lost their position too:
@dec f = new Error().stackprintedat new C (f.ts:3:15)in 1.4.2 and printsat new C (f.ts:2:1)now.toMatchInlineSnapshot()in a field initializer cannot find its call. oven-sh/bun#45677 lists the plain case as a known bug.Supersedes #435 (and its pin, oven-sh/bun#41694). That patch set
location.lineand calledsetEndPosition, whichJSTokenLocationandFunctionMetadataNodeno longer have, and it listed two frames for every static block. Positions need no change now: the stress test checks them in eval code and in functions that start in the middle of a line. I cannot close #435. Someone with the rights can.Before and after,
class W { port = missing.port }andclass S { static { S.e = new Error() } static f = new Error() }in one file:Static blocks: what was tried.
static a; static {}: 90.9M to 109.7M), step-over in the debugger jumps from the first static block to the line after the class, and coverage function totals change. Removed.StackVisitor: do not list aClassFieldInitializerModeframe whose callee is aClassStaticBlockModeframe. It gives one frame for every block with one static function, and stepping and coverage stay as they are. It costs every stack capture a check per frame:new Error()outside of any class went from 2,155 to 2,208 instructions. Not taken, because code with no class fields pays for it. The patch is 33 lines inStackVisitorif one frame per block is worth that.StaticBlockFunctionCallNode::emitBytecode(bug 245925) plans. It gives one frame, one function less per block, and no walk rule. It needs avarscope that is not a function. Not in this PR.Measurements. Two release builds of Bun 2596ac5d39 on WebKit 5a61071 that differ only by this change (both have the two new identifiers). User-mode instructions from
perf_event_open, least of three or five runs, slope between two loop counts.new C(), 1 / 4 / 16 fields, JITnew C(), 1 / 4 / 16 fields,useJIT=0class C { f = g() }: module, constructor, initializernewnew Error()in a field initializer, create.stacknew Error()in a constructor, create / create and readsizetext of the release binaryc6toc2, which is what node keeps. An Error made outside an initializer lists the same frames.CommonIdentifiers: two atoms per VM, no lookup per class.Function.prototype.callerof a sloppy function called from an initializer isnull: the initializer is strict code.Debugger.stepNextoverstatic a = f(1); static { f(2); } static b = f(3); static { f(4); } static c = f(5);stops at the same five places before and after. A pause in a block shows the name<static_initializer>for the block and for its caller, where both were empty.bun test --coverage --coverage-reporter=lcovgives the sameDA,FNFandFNHbefore and after for a class whose first static element throws ahead of blocks and fields that hold functions.jsc: the stress test with default options,--useJIT=0,--useDFGJIT=0and eager tier-up thresholds. The test pins--showPrivateScriptsInStackTraces=false, because a build that shows hidden frames lists the hidden caller of a block.Not changed here.
new W (unknown:1:11)(error: report default class constructor frames at the class definition bun#38507).Private(parser/Parser.cpp:2408), so an error in its parameter defaults has no frame.ToStringof a template substitution, a computed key,==) are still reported at the statement in front of them outside of a field. In a field they are now reported at the field.