From e9d324af5e808691f1c1ad9874f0f07e80bbddd3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 4 Oct 2026 15:51:48 -0700 Subject: [PATCH 1/5] fix(jsc): retain syntax-selected stack source positions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make JSC stack positions select the same syntax tokens as Node 24: call and constructor starts, property reads, and async `await` continuations. Keep those positions separate from exception-expression divots and debugger line tables. Both captured `StackFrame` and live `StackVisitor` readers use the metadata; executable line overrides and release-private builtin position policy are preserved. The two sparse vectors are owned by each unlinked code block, remapped through bytecode rewriting/optimization, and decoded into owned storage on both cache paths. The cache revision advances with the metadata. Property reads and calls retain distinct tokens, so `(null.x)()` can fail at `x` before reaching the call. The paired Bun adapter in [openclaw/bun#114](https://github.com/openclaw/bun/pull/114) also preserves error constructors and runtime call syntax. Rewriting `new Error()` into `Error()` moved the reported column and made a returning arrow eligible for JSC proper-tail-call elision. Preserving `new` restores the frame without disabling tail calls. Callee parentheses, computed access, and opening delimiter mappings are required for the broader normal-transpile corpus; TypeScript generic/non-null suffixes and cache invalidation are covered. This continues the source-position work in [oven-sh/bun#35179](https://github.com/oven-sh/bun/pull/35179), [#37396](https://github.com/oven-sh/bun/pull/37396), and [#41580](https://github.com/oven-sh/bun/pull/41580) by @robobun. Native Linux qualification on one c7a.24xlarge host, with the unchanged W113 lane/Docker/ICU recipe: - 1,509,853 FFI checks; 121 JSC stress configurations; interpreter/JIT/bytecode-optimizer/eager-FTL modes; owned and persistent cold/warm caches. Each cache path creates three files totaling 33,024 bytes. - Raw JSC positions improve from 13/30 to 30/30 in the call corpus and from 4/28 to 28/28 in the added cases. The live visitor matches Node at 1:22, 2:21, 3:1. Async continuation control passes. - Paired Bun: 47/47 fork-selection results with zero regressions, 343 CallSite/util/source-map tests, and 68 minifier tests. Normal transpilation matches all 30 call shapes, all 28 added cases on both stack surfaces, and 14 TypeScript/generic/non-null/astral cases. The JavaScript/TypeScript module corpus and 58 raw-eval rows match as well. - Runtime cache replacement and warm replay match all 56 added observations. Existing custom-stack generated-versus-mapped and filename/eval-origin policies are preserved. - OpenClaw loader pair 8/8, SDK under its existing native-loader policy 67 pass/1 skip, Slack ordered shared-worker sequence 26/26, and real Proxyline probe pass. The original shared SDK policy retains its same two baseline tsconfig resolver failures. - ABBA, eight samples per arm: exception creation/formatting +7.6%; fresh 350-module startup 0.998× baseline. The exception overhead is an explicit tradeoff. - Final complete engine and Bun branch reviews are scoped-clean through P2. Exact-head [CI run 37240058657](https://github.com/openclaw/WebKit/actions/runs/37240058657) is green at `6e69d757023aa75398a670f487f90fe2a57e2b99`. Qualification caught and corrected a namespace-only result-checker assumption, repeated C++ default arguments in unity builds, private-builtin position leakage, missing transpiler delimiter metadata, TypeScript non-null suffix tracking, and the omitted live-stack reader. Exact position goldens were checked against Node; the original astral inline-snapshot assertion remains intact and passes. The Bun draft remains stacked on manifest commit `cf636c2f14914b3ba4b319874312d654cc5ea064` and also needs the namespace adapter from Bun #106. Native baseline and candidate both include that adapter because current engine main requires its API. This PR does not publish artifacts or change the artifact recipe; batch publication stays with the coordinator. --- .github/openclaw/check-stack-visitor.py | 16 + .../patches/009-stack-positions.patch | 1342 +++++++++++++++++ .../openclaw/qualification/stack-visitor.js | 3 + .github/openclaw/qualify.sh | 2 + .github/openclaw/test_verify_jsc_results.py | 41 + .github/openclaw/verify-jsc-results.py | 18 +- .github/workflows/openclaw-checks.yml | 14 + JSTests/stress/callsite-async-position.mjs | 12 + JSTests/stress/callsite-syntax-positions.js | 116 ++ JSTests/stress/stack-position-regressions.js | 104 ++ OPENCLAW.md | 8 + .../bytecode/BytecodeOptimizer.cpp | 12 + .../bytecode/BytecodeRewriter.h | 11 + Source/JavaScriptCore/bytecode/CodeBlock.cpp | 8 + Source/JavaScriptCore/bytecode/CodeBlock.h | 1 + .../bytecode/UnlinkedCodeBlock.cpp | 15 + .../bytecode/UnlinkedCodeBlock.h | 5 + .../bytecode/UnlinkedCodeBlockGenerator.cpp | 16 + .../bytecode/UnlinkedCodeBlockGenerator.h | 12 + .../bytecompiler/BytecodeGenerator.cpp | 63 +- .../bytecompiler/BytecodeGenerator.h | 42 +- .../bytecompiler/NodesCodegen.cpp | 74 +- .../interpreter/Interpreter.cpp | 12 +- .../interpreter/StackVisitor.cpp | 5 +- Source/JavaScriptCore/parser/ASTBuilder.h | 26 + Source/JavaScriptCore/parser/Nodes.h | 8 + Source/JavaScriptCore/parser/Parser.cpp | 7 + Source/JavaScriptCore/parser/SyntaxChecker.h | 3 + Source/JavaScriptCore/runtime/CachedTypes.cpp | 13 +- Source/JavaScriptCore/runtime/StackFrame.cpp | 5 +- 30 files changed, 1933 insertions(+), 81 deletions(-) create mode 100644 .github/openclaw/check-stack-visitor.py create mode 100644 .github/openclaw/qualification/patches/009-stack-positions.patch create mode 100644 .github/openclaw/qualification/stack-visitor.js create mode 100644 .github/openclaw/test_verify_jsc_results.py create mode 100644 JSTests/stress/callsite-async-position.mjs create mode 100644 JSTests/stress/callsite-syntax-positions.js create mode 100644 JSTests/stress/stack-position-regressions.js diff --git a/.github/openclaw/check-stack-visitor.py b/.github/openclaw/check-stack-visitor.py new file mode 100644 index 0000000000000..19ac16b5b57ec --- /dev/null +++ b/.github/openclaw/check-stack-visitor.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +"""Pin the live stack visitor to the same call tokens as captured stacks.""" +import json +import re +import subprocess +import sys +from pathlib import Path + +fixture = Path(__file__).with_name("qualification") / "stack-visitor.js" +expected = [(1, 22), (2, 21), (3, 1)] +for mode, flags in [("default", []), ("interpreter", ["--useJIT=false"])]: + result = subprocess.run([sys.argv[1], *flags, str(fixture)], capture_output=True, text=True, check=True) + actual = [(int(line), int(column)) for line, column in re.findall(r"stack-visitor\.js:(\d+):(\d+)", result.stderr)] + if actual != expected: + raise SystemExit(f"{mode}: expected {expected}, got {actual}\n{result.stderr}") + print(json.dumps({"mode": mode, "positions": actual, "passed": True})) diff --git a/.github/openclaw/qualification/patches/009-stack-positions.patch b/.github/openclaw/qualification/patches/009-stack-positions.patch new file mode 100644 index 0000000000000..cb44adcf99074 --- /dev/null +++ b/.github/openclaw/qualification/patches/009-stack-positions.patch @@ -0,0 +1,1342 @@ +diff --git a/CHANGELOG.md b/CHANGELOG.md +index e6e2a21a..5a296061 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +@@ -2,6 +2,8 @@ + + ## Unreleased + ++- Preserve built-in error constructor syntax and returning arrow frames, and use JSC syntax-selected call and property-read stack positions. Retain runtime callee parentheses and computed access, and map call, bracket, and template delimiters back to their original source. Continues the source-position fixes from [oven-sh/bun#35179](https://github.com/oven-sh/bun/pull/35179), [oven-sh/bun#37396](https://github.com/oven-sh/bun/pull/37396), and [oven-sh/bun#41580](https://github.com/oven-sh/bun/pull/41580). Thanks @robobun! ++ + - Align WebKit and VM source-position assertions with Node's constructor locations and select these regressions for native stack-formatting changes in both fork CI lanes. + + - Preserve the native-context count in the fast `v8.getHeapStatistics()` adapter and avoid appending a second strict code-generation flag to inherited worker arguments. +diff --git a/docs/runtime/nodejs-compat.mdx b/docs/runtime/nodejs-compat.mdx +index 0274c5b4..e94321aa 100644 +--- a/docs/runtime/nodejs-compat.mdx ++++ b/docs/runtime/nodejs-compat.mdx +@@ -19,7 +19,7 @@ The OpenClaw fork reports missing runtime imports without downloading npm packag + + `Error.prepareStackTrace` supports deletion, descriptor restoration, and accessor replacement. Nested stack formatting and throwing formatters leave later captures usable. + +-`CallSite.getLineNumber()` and `getColumnNumber()` return one-based positions, or `null` when a frame has no source position. Stack strings include column 1 and use the same constructor positions as CallSites, pointing to the `new` keyword, including across line breaks and in `node:vm` scripts. Some expression positions still differ from Node.js because JavaScriptCore and Bun's transpiler select different syntax locations. ++`CallSite.getLineNumber()` and `getColumnNumber()` return one-based positions, or `null` when a frame has no source position. Stack strings include column 1 and use the same constructor positions as CallSites, pointing to the `new` keyword, including across line breaks and in `node:vm` scripts. The matching OpenClaw engine supplies syntax-selected positions for calls, constructors, property reads, and async continuations. Built-in error constructors retain `new`, including in returning arrows. Runtime transpilation retains callee parentheses and computed access and maps opening call, bracket, and template delimiters to the original source. File-name and eval-origin formatting still follow Bun's existing conventions. + + ### [`node:assert`](https://nodejs.org/api/assert.html) + +diff --git a/src/ast/e.rs b/src/ast/e.rs +index 520c12f6..6db08699 100644 +--- a/src/ast/e.rs ++++ b/src/ast/e.rs +@@ -259,7 +259,9 @@ pub struct Call { + pub args: ExprNodeList, + pub optional_chain: Option, + pub is_direct_eval: bool, ++ pub open_paren_loc: crate::Loc, + pub close_paren_loc: crate::Loc, ++ pub preserve_parenthesized_target: bool, + + /// True if there is a comment containing "@__PURE__" or "#__PURE__" preceding + /// this call expression. This is an annotation used for tree shaking, and +@@ -281,7 +283,9 @@ impl Default for Call { + args: bun_alloc::AstAlloc::vec(), + optional_chain: None, + is_direct_eval: false, ++ open_paren_loc: crate::Loc::EMPTY, + close_paren_loc: crate::Loc::EMPTY, ++ preserve_parenthesized_target: false, + can_be_unwrapped_if_unused: CallUnwrap::Never, + was_jsx_element: false, + } +@@ -334,6 +338,7 @@ impl Default for Dot { + } + } + pub struct Index { ++ pub open_bracket_loc: crate::Loc, + pub index: ExprNodeIndex, + pub target: ExprNodeIndex, + pub optional_chain: Option, +@@ -2114,6 +2119,7 @@ pub struct TemplatePart { + } + + pub struct Template { ++ pub head_loc: crate::Loc, + pub tag: Option, + /// Arena-owned mutable slice. Stored as a + /// `StoreSlice` so writers (`substitute_single_use_symbol_in_expr`, the +@@ -2335,6 +2341,7 @@ impl Template { + // provenance for downstream mutators. + Expr::init( + Template { ++ head_loc: crate::Loc::EMPTY, + tag: None, + parts: crate::StoreSlice::from_bump(parts), + head: TemplateContents::Cooked( +diff --git a/src/ast/expr.rs b/src/ast/expr.rs +index cfedeb4f..f24c2239 100644 +--- a/src/ast/expr.rs ++++ b/src/ast/expr.rs +@@ -2000,7 +2000,9 @@ impl Data { + .try_deep_clone_with(|e| e.deep_clone_no_detach(bump))?, + optional_chain: el.optional_chain, + is_direct_eval: el.is_direct_eval, ++ open_paren_loc: el.open_paren_loc, + close_paren_loc: el.close_paren_loc, ++ preserve_parenthesized_target: el.preserve_parenthesized_target, + can_be_unwrapped_if_unused: el.can_be_unwrapped_if_unused, + was_jsx_element: el.was_jsx_element, + }); +@@ -2020,6 +2022,7 @@ impl Data { + } + Data::EIndex(el) => { + let item = bump.alloc(E::Index { ++ open_bracket_loc: el.open_bracket_loc, + target: el.target.deep_clone_no_detach(bump)?, + index: el.index.deep_clone_no_detach(bump)?, + optional_chain: el.optional_chain, +@@ -2080,6 +2083,7 @@ impl Data { + } + Data::ETemplate(el) => { + let item = bump.alloc(E::Template { ++ head_loc: el.head_loc, + tag: match &el.tag { + Some(tag) => Some(tag.deep_clone_no_detach(bump)?), + None => None, +diff --git a/src/ast/fold_string_addition.rs b/src/ast/fold_string_addition.rs +index e096e0a3..85fb1615 100644 +--- a/src/ast/fold_string_addition.rs ++++ b/src/ast/fold_string_addition.rs +@@ -93,6 +93,7 @@ pub fn fold_string_addition( + if right.head.is_utf8() { + return Some(Expr::init( + E::Template { ++ head_loc: crate::Loc::EMPTY, + tag: None, + parts: right.parts, + head: e::TemplateContents::Cooked(join_strings( +diff --git a/src/ast/known_global.rs b/src/ast/known_global.rs +index 15c439d7..a69b7697 100644 +--- a/src/ast/known_global.rs ++++ b/src/ast/known_global.rs +@@ -103,7 +103,7 @@ impl KnownGlobal { + let constructor = lookup(original_name)?; + + match constructor { +- // Error constructors can be called without 'new' with identical behavior ++ // Removing `new` changes stack positions and makes returned errors eligible for tail calls. + KnownGlobal::Error + | KnownGlobal::TypeError + | KnownGlobal::SyntaxError +@@ -111,10 +111,7 @@ impl KnownGlobal { + | KnownGlobal::ReferenceError + | KnownGlobal::EvalError + | KnownGlobal::URIError +- | KnownGlobal::AggregateError => { +- // Convert `new Error(...)` to `Error(...)` to save bytes +- Some(Self::call_from_new(e, loc)) +- } ++ | KnownGlobal::AggregateError => None, + + KnownGlobal::Object => { + let n = e.args.len_u32(); +diff --git a/src/bundler/linker_context/generateCodeForLazyExport.rs b/src/bundler/linker_context/generateCodeForLazyExport.rs +index 44c9a361..ff5d0edf 100644 +--- a/src/bundler/linker_context/generateCodeForLazyExport.rs ++++ b/src/bundler/linker_context/generateCodeForLazyExport.rs +@@ -344,6 +344,7 @@ pub(crate) fn generate_code_for_lazy_export( + bun_ast::StoreSlice::new_mut(arena.alloc_slice_fill_iter(template_parts)); + value = Expr::init( + E::Template { ++ head_loc: bun_ast::Loc::EMPTY, + tag: None, + parts: parts_slice, + head: E::TemplateContents::Cooked(E::String::init(b"")), +diff --git a/src/js_parser/lower/lower_decorators.rs b/src/js_parser/lower/lower_decorators.rs +index c285fbfb..dff0909c 100644 +--- a/src/js_parser/lower/lower_decorators.rs ++++ b/src/js_parser/lower/lower_decorators.rs +@@ -1142,6 +1142,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let this = p.new_expr(E::This {}, loc); + let member = p.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: this, + index: key, + optional_chain: None, +@@ -1438,6 +1439,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + ); + self.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: object, + index: key, + optional_chain: None, +diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs +index 7da4223f..ae67d9a1 100644 +--- a/src/js_parser/p.rs ++++ b/src/js_parser/p.rs +@@ -178,6 +178,8 @@ pub(crate) struct ParserSnapshot<'a> { + should_fold_typescript_constant_expressions: bool, + fn_or_arrow_data_parse: FnOrArrowDataParse, + latest_arrow_arg_loc: bun_ast::Loc, ++ parenthesized_target_loc: bun_ast::Loc, ++ parenthesized_suffix_loc: bun_ast::Loc, + forbid_suffix_after_as_loc: bun_ast::Loc, + after_arrow_body_loc: bun_ast::Loc, + esm_import_keyword: bun_ast::Range, +@@ -271,6 +273,8 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> { + // allocated_names: ListManaged(string) = ListManaged(string).init(bun.default_allocator), + // allocated_names_pool: ?*AllocatedNamesPool.Node = null, + pub(crate) latest_arrow_arg_loc: bun_ast::Loc, ++ pub(crate) parenthesized_target_loc: bun_ast::Loc, ++ pub(crate) parenthesized_suffix_loc: bun_ast::Loc, + pub(crate) forbid_suffix_after_as_loc: bun_ast::Loc, + pub(crate) current_scope: js_ast::StoreRef, + pub(crate) scopes_for_current_part: List<'a, *mut js_ast::Scope>, +@@ -5486,7 +5490,16 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + }); + } + ++ pub(crate) fn preserve_parenthesized_call_target(&self, expr: Expr) -> bool { ++ self.options.features.dont_bundle_twice ++ && self.parenthesized_target_loc.eql(expr.loc) ++ && self.parenthesized_suffix_loc.eql(self.lexer.loc()) ++ } ++ + pub(crate) fn mark_expr_as_parenthesized(&mut self, expr: &mut Expr) { ++ // Runtime loads retain the callee parentheses that select JSC's call position. ++ self.parenthesized_target_loc = expr.loc; ++ self.parenthesized_suffix_loc = self.lexer.loc(); + match &mut expr.data { + js_ast::ExprData::EArray(ex) => ex.is_parenthesized = true, + js_ast::ExprData::EObject(ex) => ex.is_parenthesized = true, +@@ -7564,6 +7577,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + } + _ => self.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target, + index: key, + optional_chain: None, +@@ -8351,6 +8365,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + .should_fold_typescript_constant_expressions, + fn_or_arrow_data_parse: self.fn_or_arrow_data_parse.clone(), + latest_arrow_arg_loc: self.latest_arrow_arg_loc, ++ parenthesized_target_loc: self.parenthesized_target_loc, ++ parenthesized_suffix_loc: self.parenthesized_suffix_loc, + forbid_suffix_after_as_loc: self.forbid_suffix_after_as_loc, + after_arrow_body_loc: self.after_arrow_body_loc, + esm_import_keyword: self.esm_import_keyword, +@@ -8385,6 +8401,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + snapshot.should_fold_typescript_constant_expressions; + self.fn_or_arrow_data_parse = snapshot.fn_or_arrow_data_parse; + self.latest_arrow_arg_loc = snapshot.latest_arrow_arg_loc; ++ self.parenthesized_target_loc = snapshot.parenthesized_target_loc; ++ self.parenthesized_suffix_loc = snapshot.parenthesized_suffix_loc; + self.forbid_suffix_after_as_loc = snapshot.forbid_suffix_after_as_loc; + self.after_arrow_body_loc = snapshot.after_arrow_body_loc; + self.esm_import_keyword = snapshot.esm_import_keyword; +@@ -9841,6 +9859,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + fn_only_data_visit: FnOnlyDataVisit::default(), + allocated_names: BumpVec::new_in(arena), + latest_arrow_arg_loc: bun_ast::Loc::EMPTY, ++ parenthesized_target_loc: bun_ast::Loc::EMPTY, ++ parenthesized_suffix_loc: bun_ast::Loc::EMPTY, + forbid_suffix_after_as_loc: bun_ast::Loc::EMPTY, + scopes_for_current_part: BumpVec::new_in(arena), + symbols: BumpVec::with_capacity_in(estimated_symbol_count, arena), +diff --git a/src/js_parser/parse/parse_prefix.rs b/src/js_parser/parse/parse_prefix.rs +index 5c4d54f7..2ff66c10 100644 +--- a/src/js_parser/parse/parse_prefix.rs ++++ b/src/js_parser/parse/parse_prefix.rs +@@ -54,8 +54,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + p.allow_in = true; + + let mut value = p.parse_expr(Level::Lowest)?; +- p.mark_expr_as_parenthesized(&mut value); + p.lexer.expect(T::TCloseParen)?; ++ p.mark_expr_as_parenthesized(&mut value); + + p.allow_in = old_allow_in; + return Ok(value); +@@ -295,6 +295,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + + Ok(p.new_expr( + E::Template { ++ head_loc: loc, + tag: None, + head: E::TemplateContents::Cooked(head), + parts, +diff --git a/src/js_parser/parse/parse_suffix.rs b/src/js_parser/parse/parse_suffix.rs +index d6b6bd15..856d7d13 100644 +--- a/src/js_parser/parse/parse_suffix.rs ++++ b/src/js_parser/parse/parse_suffix.rs +@@ -83,6 +83,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let index = p.new_expr(E::PrivateIdentifier { ref_ }, name_loc); + *left = p.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target, + index, + optional_chain: old_optional_chain, +@@ -140,6 +141,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + match p.lexer.token { + T::TOpenBracket => { + // "a?.[b]" ++ let open_bracket_loc = p.lexer.loc(); + p.lexer.next()?; + + // allow "in" inside the brackets; +@@ -155,6 +157,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let target = *left; + *left = p.new_expr( + E::Index { ++ open_bracket_loc, + target, + index, + optional_chain: optional_start, +@@ -170,6 +173,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + return Ok(Continuation::Done); + } + ++ let open_paren_loc = p.lexer.loc(); ++ let preserve_parenthesized_target = p.preserve_parenthesized_call_target(*left); + let list_loc = p.parse_call_args()?; + let loc = left.loc; + let target = *left; +@@ -177,7 +182,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + E::Call { + target, + args: list_loc.list, ++ open_paren_loc, + close_paren_loc: list_loc.loc, ++ preserve_parenthesized_target, + optional_chain: optional_start, + ..Default::default() + }, +@@ -200,6 +207,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + return Ok(Continuation::Done); + } + ++ let open_paren_loc = p.lexer.loc(); ++ let preserve_parenthesized_target = p.preserve_parenthesized_call_target(*left); + let list_loc = p.parse_call_args()?; + let loc = left.loc; + let target = *left; +@@ -207,7 +216,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + E::Call { + target, + args: list_loc.list, ++ open_paren_loc, + close_paren_loc: list_loc.loc, ++ preserve_parenthesized_target, + optional_chain: optional_start, + ..Default::default() + }, +@@ -226,6 +237,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let index = p.new_expr(E::PrivateIdentifier { ref_ }, name_loc); + *left = p.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target, + index, + optional_chain: optional_start, +@@ -281,6 +293,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + ); + } + // p.markSyntaxFeature(compat.TemplateLiteral, p.lexer.Range()); ++ let head_loc = p.lexer.loc(); + let head = E::Str::new(p.lexer.raw_template_contents()); + p.lexer.next()?; + +@@ -288,6 +301,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let tag = *left; + *left = p.new_expr( + E::Template { ++ head_loc, + tag: Some(tag), + head: E::TemplateContents::Raw(head), + parts: E::Template::empty_parts(), +@@ -312,12 +326,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + ); + } + // p.markSyntaxFeature(compat.TemplateLiteral, p.lexer.Range()); ++ let head_loc = p.lexer.loc(); + let head = E::Str::new(p.lexer.raw_template_contents()); + let (parts, _tail_loc) = p.parse_template_parts(true)?; + let tag = *left; + let loc = left.loc; + *left = p.new_expr( + E::Template { ++ head_loc, + tag: Some(tag), + head: E::TemplateContents::Raw(head), + parts, +@@ -346,6 +362,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + return Ok(Continuation::Done); + } + ++ let open_bracket_loc = p.lexer.loc(); + p.lexer.next()?; + + // Allow "in" inside the brackets +@@ -362,6 +379,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let target = *left; + *left = p.new_expr( + E::Index { ++ open_bracket_loc, + target, + index, + optional_chain: old_optional_chain, +@@ -384,6 +402,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + return Ok(Continuation::Done); + } + ++ let open_paren_loc = p.lexer.loc(); ++ let preserve_parenthesized_target = p.preserve_parenthesized_call_target(*left); + let list_loc = p.parse_call_args()?; + let loc = left.loc; + let target = *left; +@@ -391,7 +411,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + E::Call { + target, + args: list_loc.list, ++ open_paren_loc, + close_paren_loc: list_loc.loc, ++ preserve_parenthesized_target, + optional_chain: old_optional_chain, + ..Default::default() + }, +@@ -486,6 +508,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + p: &mut Self, + optional_chain: &mut Option, + old_optional_chain: Option, ++ left: &Expr, + ) -> CResult { + // Skip over TypeScript non-null assertions + if p.lexer.has_newline_before { +@@ -497,7 +520,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + return Err(crate::Error::SyntaxError); + } + ++ let parenthesized = p.preserve_parenthesized_call_target(*left); + p.lexer.next()?; ++ if parenthesized { ++ p.parenthesized_suffix_loc = p.lexer.loc(); ++ } + *optional_chain = old_optional_chain; + + Ok(Continuation::Next) +@@ -878,8 +905,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + // TypeScript allows type arguments to be specified with angle brackets + // inside an expression. Unlike in other languages, this unfortunately + // appears to require backtracking to parse. ++ let parenthesized = p.preserve_parenthesized_call_target(*left); + if Self::IS_TYPESCRIPT_ENABLED && p.try_skip_type_script_type_arguments_with_backtracking() + { ++ if parenthesized { ++ p.parenthesized_suffix_loc = p.lexer.loc(); ++ } + *optional_chain = old_optional_chain; + return Ok(Continuation::Next); + } +@@ -969,8 +1000,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + // TypeScript allows type arguments to be specified with angle brackets + // inside an expression. Unlike in other languages, this unfortunately + // appears to require backtracking to parse. ++ let parenthesized = p.preserve_parenthesized_call_target(*left); + if Self::IS_TYPESCRIPT_ENABLED && p.try_skip_type_script_type_arguments_with_backtracking() + { ++ if parenthesized { ++ p.parenthesized_suffix_loc = p.lexer.loc(); ++ } + *optional_chain = old_optional_chain; + return Ok(Continuation::Next); + } +@@ -1555,7 +1590,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + T::TSlash => Self::sfx_t_slash(p, level, left), + T::TSlashEquals => Self::sfx_t_slash_equals(p, level, left), + T::TExclamation => { +- Self::sfx_t_exclamation(p, &mut optional_chain, old_optional_chain) ++ Self::sfx_t_exclamation(p, &mut optional_chain, old_optional_chain, left) + } + T::TBarBar => Self::sfx_t_bar_bar(p, level, left, flags), + T::TAmpersandAmpersand => Self::sfx_t_ampersand_ampersand(p, level, left, flags), +diff --git a/src/js_parser/parse/parse_typescript.rs b/src/js_parser/parse/parse_typescript.rs +index eb4d49f9..7acac3ed 100644 +--- a/src/js_parser/parse/parse_typescript.rs ++++ b/src/js_parser/parse/parse_typescript.rs +@@ -136,6 +136,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let index = p.new_expr(E::PrivateIdentifier { ref_ }, name_loc); + expr = p.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: expr, + index, + optional_chain: None, +diff --git a/src/js_parser/visit/mod.rs b/src/js_parser/visit/mod.rs +index e12fd145..ae13f46c 100644 +--- a/src/js_parser/visit/mod.rs ++++ b/src/js_parser/visit/mod.rs +@@ -1350,6 +1350,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + } + _ => self.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: this_target, + index: key, + optional_chain: None, +diff --git a/src/js_parser/visit/visit_expr.rs b/src/js_parser/visit/visit_expr.rs +index 23c08903..c5e0d61a 100644 +--- a/src/js_parser/visit/visit_expr.rs ++++ b/src/js_parser/visit/visit_expr.rs +@@ -855,8 +855,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let is_template_tag = matches!(p.template_tag, Data::EIndex(tag) if core::ptr::eq(&raw const *e_, &raw const *tag)); + let is_delete_target = matches!(p.delete_target, Data::EIndex(dt) if core::ptr::eq(&raw const *e_, &raw const *dt)); + +- // "a['b']" => "a.b" +- if p.options.features.minify_syntax { ++ // Runtime stack positions distinguish computed access from dot access. ++ if p.options.features.minify_syntax && !p.options.features.dont_bundle_twice { + if let Some(mut s) = e_.index.data.e_string() { + if !s.is_utf16 && s.is_identifier(p.arena) { + let dot = p.new_expr( +@@ -985,7 +985,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + + // "a['b' + '']" => "a.b" + // "enum A { B = 'b' }; a[A.B]" => "a.b" +- if p.options.features.minify_syntax && s.is_identifier(p.arena) { ++ if p.options.features.minify_syntax ++ && !p.options.features.dont_bundle_twice ++ && s.is_identifier(p.arena) ++ { + let dot = p.new_expr( + E::Dot { + name: s.data, +diff --git a/src/js_parser/visit/visit_stmt.rs b/src/js_parser/visit/visit_stmt.rs +index f3995507..33a4cca1 100644 +--- a/src/js_parser/visit/visit_stmt.rs ++++ b/src/js_parser/visit/visit_stmt.rs +@@ -2332,6 +2332,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + Expr::assign( + p.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: Expr::init_identifier(data.arg, value.loc), + index: name_as_e_string.unwrap(), + optional_chain: None, +@@ -2353,6 +2354,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + value_exprs.push(Expr::assign( + p.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: Expr::init_identifier(data.arg, value.loc), + index: assign_target, + optional_chain: None, +diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs +index 40408623..764a2900 100644 +--- a/src/js_printer/lib.rs ++++ b/src/js_printer/lib.rs +@@ -3502,6 +3502,9 @@ pub(crate) mod __gated_printer { + && self.is_unbound_eval_identifier(e.target) + && e.optional_chain.is_none(); + ++ if e.preserve_parenthesized_target { ++ self.print(b"("); ++ } + if is_unbound_eval { + self.print(b"(0,"); + self.print_space(); +@@ -3513,9 +3516,15 @@ pub(crate) mod __gated_printer { + self.print_expr(e.target, Level::Postfix, target_flags); + } + ++ if e.preserve_parenthesized_target { ++ self.print(b")"); ++ } + if e.optional_chain == Some(js_ast::OptionalChain::Start) { + self.print(b"?."); + } ++ if !e.open_paren_loc.is_empty() { ++ self.add_source_mapping(e.open_paren_loc); ++ } + self.print(b"("); + let args = e.args.slice(); + +@@ -3765,6 +3774,9 @@ pub(crate) mod __gated_printer { + self.print_symbol(priv_.ref_); + } + _ => { ++ if !e.open_bracket_loc.is_empty() { ++ self.add_source_mapping(e.open_bracket_loc); ++ } + self.print(b"["); + self.add_source_mapping(e.index.loc); + self.print_expr(e.index, Level::Lowest, ExprFlag::none()); +@@ -4063,6 +4075,7 @@ pub(crate) mod __gated_printer { + // would be a cross-thread data race. Re-prints recompute + // the identical fold, so emitted output is unchanged. + let mut e = E::Template { ++ head_loc: e.head_loc, + tag: e.tag, + parts: e.parts, + head: match &e.head { +@@ -4128,6 +4141,7 @@ pub(crate) mod __gated_printer { + // parts slice stays valid past this block. + let parts_slice = js_ast::StoreSlice::from_bump(replaced); + let mut copy = E::Template { ++ head_loc: e.head_loc, + tag: e.tag, + parts: parts_slice, + head: match &e.head { +@@ -4157,6 +4171,7 @@ pub(crate) mod __gated_printer { + // fields out is safe even though `t` itself may + // point at `copy`. + e = E::Template { ++ head_loc: t.head_loc, + tag: t.tag, + parts: t.parts, + head: match &t.head { +@@ -4204,6 +4219,9 @@ pub(crate) mod __gated_printer { + self.add_source_mapping(expr.loc); + } + ++ if !e.head_loc.is_empty() { ++ self.add_source_mapping(e.head_loc); ++ } + self.print(b"`"); + match &e.head { + E::TemplateContents::Raw(raw) => self.print_raw_template_literal(raw), +diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs +index 1142ca3b..c857749a 100644 +--- a/src/jsc/RuntimeTranspilerCache.rs ++++ b/src/jsc/RuntimeTranspilerCache.rs +@@ -66,7 +66,9 @@ bun_core::declare_scope!(cache, visible); + /// Version 34: Discard output containing a previous runtime plugin's resolved import paths. + /// Version 35: Preserve inferred names and inline type-only runtime evaluation edges. + /// Version 36: Runtime plugin imports retain their original specifiers and resolve when executed. +-const EXPECTED_VERSION: u32 = 36; ++/// Version 37: Preserve builtin error constructors and their stack positions and frames. ++/// Version 38: Retain runtime call syntax and map opening call/property delimiters. ++const EXPECTED_VERSION: u32 = 38; + + /// Source files smaller than this are not written to / read from the on-disk + /// transpiler cache. Originally 50 KiB, which excluded almost every file in a +diff --git a/src/jsc/bindings/ErrorStackFrame.cpp b/src/jsc/bindings/ErrorStackFrame.cpp +index 14ec9fd8..9f8c3616 100644 +--- a/src/jsc/bindings/ErrorStackFrame.cpp ++++ b/src/jsc/bindings/ErrorStackFrame.cpp +@@ -22,12 +22,13 @@ static bool isConstruct(JSC::CodeBlock* code, JSC::BytecodeIndex bc) + } + } + +-ZigStackFramePosition getAdjustedPositionForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc) ++ZigStackFramePosition getAdjustedPositionForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc, bool isAsync) + { + auto expr = code->expressionInfoForBytecodeIndex(bc); +- auto offset = expr.divot; +- // Constructors point to `new`; other calls retain JSC's syntax location. +- if (isConstruct(code, bc)) ++ auto callSitePosition = code->callSitePositionForBytecodeIndex(bc, isAsync); ++ auto offset = callSitePosition.value_or(expr.divot); ++ // Older or implicit bytecode can lack a syntax-selected stack position. ++ if (!callSitePosition && isConstruct(code, bc)) + offset -= std::min(offset, expr.startOffset); + + auto lineColumn = code->source().provider()->documentLineColumnForOffset(offset); +@@ -38,16 +39,18 @@ ZigStackFramePosition getAdjustedPositionForBytecode(JSC::CodeBlock* code, JSC:: + }; + } + +-ZigStackFramePosition getAdjustedLineColumnForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc) ++ZigStackFramePosition getAdjustedLineColumnForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc, bool isAsync) + { + if (isConstruct(code, bc)) { +- auto position = getAdjustedPositionForBytecode(code, bc); ++ auto position = getAdjustedPositionForBytecode(code, bc, isAsync); + position.byte_position = -1; + return position; + } + +- // Keep JSC's cached lookup for frames that do not need an expression-range adjustment. +- auto lineColumn = code->lineColumnForBytecodeIndex(bc); ++ auto callSitePosition = code->callSitePositionForBytecodeIndex(bc, isAsync); ++ auto lineColumn = callSitePosition ++ ? code->source().provider()->documentLineColumnForOffset(*callSitePosition) ++ : code->lineColumnForBytecodeIndex(bc); + return { + .line_zero_based = OrdinalNumber::fromOneBasedInt(lineColumn.line).zeroBasedInt(), + .column_zero_based = OrdinalNumber::fromOneBasedInt(lineColumn.column).zeroBasedInt(), +diff --git a/src/jsc/bindings/ErrorStackFrame.h b/src/jsc/bindings/ErrorStackFrame.h +index a1c31678..70f1b2fa 100644 +--- a/src/jsc/bindings/ErrorStackFrame.h ++++ b/src/jsc/bindings/ErrorStackFrame.h +@@ -4,8 +4,8 @@ + + namespace Bun { + +-ZigStackFramePosition getAdjustedPositionForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc); ++ZigStackFramePosition getAdjustedPositionForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc, bool isAsync); + +-ZigStackFramePosition getAdjustedLineColumnForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc); ++ZigStackFramePosition getAdjustedLineColumnForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc, bool isAsync); + + } // namespace Bun +diff --git a/src/jsc/bindings/ErrorStackTrace.cpp b/src/jsc/bindings/ErrorStackTrace.cpp +index 3550bb52..866d011c 100644 +--- a/src/jsc/bindings/ErrorStackTrace.cpp ++++ b/src/jsc/bindings/ErrorStackTrace.cpp +@@ -341,7 +341,7 @@ bool JSCStackFrame::calculateSourcePositions() + return false; + } + +- auto location = Bun::getAdjustedLineColumnForBytecode(m_codeBlock, m_bytecodeIndex); ++ auto location = Bun::getAdjustedLineColumnForBytecode(m_codeBlock, m_bytecodeIndex, isAsync()); + m_sourcePositions.line = location.line(); + m_sourcePositions.column = location.column(); + +diff --git a/src/jsc/bindings/FormatStackTraceForJS.cpp b/src/jsc/bindings/FormatStackTraceForJS.cpp +index 60aca2e7..403bf131 100644 +--- a/src/jsc/bindings/FormatStackTraceForJS.cpp ++++ b/src/jsc/bindings/FormatStackTraceForJS.cpp +@@ -321,7 +321,7 @@ WTF::String formatStackTrace( + if (!frame.hasLineAndColumnInfo()) continue; + + if (frame.codeBlock() && frame.hasBytecodeIndex()) { +- auto position = Bun::getAdjustedLineColumnForBytecode(frame.codeBlock(), frame.bytecodeIndex()); ++ auto position = Bun::getAdjustedLineColumnForBytecode(frame.codeBlock(), frame.bytecodeIndex(), frame.isAsyncFrame()); + originalLineColumns[i] = { static_cast(position.line().oneBasedInt()), static_cast(position.column().oneBasedInt()) }; + if (auto overrideLine = frame.codeBlock()->ownerExecutable()->overrideLineNumber(vm)) + originalLineColumns[i].line = *overrideLine; +diff --git a/src/jsc/bindings/ZigException.cpp b/src/jsc/bindings/ZigException.cpp +index cfd1059c..b7442cb3 100644 +--- a/src/jsc/bindings/ZigException.cpp ++++ b/src/jsc/bindings/ZigException.cpp +@@ -143,7 +143,7 @@ static void populateStackFramePosition(const JSC::StackFrame& stackFrame, BunStr + return; + } + +- auto location = Bun::getAdjustedPositionForBytecode(code, stackFrame.bytecodeIndex()); ++ auto location = Bun::getAdjustedPositionForBytecode(code, stackFrame.bytecodeIndex(), stackFrame.isAsyncFrame()); + memcpy(&position, &location, sizeof(ZigStackFramePosition)); + if (flags == PopulateStackTraceFlags::OnlyPosition) + return; +diff --git a/src/react_compiler/codegen.rs b/src/react_compiler/codegen.rs +index 473b14c9..5b3f1704 100644 +--- a/src/react_compiler/codegen.rs ++++ b/src/react_compiler/codegen.rs +@@ -669,6 +669,7 @@ fn codegen_reactive_scope( + let cache_slot = |index: u32| { + Expr::init( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: cache_ident(), + index: Expr::init(E::Number::new(index as f64), loc), + optional_chain: None, +@@ -2005,6 +2006,7 @@ fn codegen_base_instruction_value( + let prop = codegen_place_to_expression(cx, property)?; + Ok(Expr::init( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: obj, + index: prop, + optional_chain: None, +@@ -2027,6 +2029,7 @@ fn codegen_base_instruction_value( + op: OpCode::BinAssign, + left: Expr::init( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: obj, + index: prop, + optional_chain: None, +@@ -2049,6 +2052,7 @@ fn codegen_base_instruction_value( + op: OpCode::UnDelete, + value: Expr::init( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: obj, + index: prop, + optional_chain: None, +@@ -2179,6 +2183,7 @@ fn codegen_base_instruction_value( + let tag_expr = codegen_place_to_expression(cx, tag)?; + Ok(Expr::init( + E::Template { ++ head_loc: bun_ast::Loc::EMPTY, + tag: Some(tag_expr), + head: E::TemplateContents::Raw(value.raw), + parts: StoreSlice::EMPTY, +@@ -2204,6 +2209,7 @@ fn codegen_base_instruction_value( + } + Ok(Expr::init( + E::Template { ++ head_loc: bun_ast::Loc::EMPTY, + tag: None, + head, + parts: StoreSlice::new_mut(parts.leak()), +@@ -2369,6 +2375,7 @@ fn codegen_function_expression( + }); + let wrapped = Expr::init( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: Expr::init( + E::Object { + properties: props, +@@ -3311,6 +3318,7 @@ fn property_access_expr( + ), + PropertyLiteral::Number(n) => Expr::init( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target, + index: Expr::init(E::Number::new(n.value()), loc), + optional_chain, +diff --git a/test/bundler/bundler_minify.test.ts b/test/bundler/bundler_minify.test.ts +index 052aa492..0d629bee 100644 +--- a/test/bundler/bundler_minify.test.ts ++++ b/test/bundler/bundler_minify.test.ts +@@ -1229,7 +1229,7 @@ describe("bundler", () => { + }, + }); + +- itBundled("minify/ErrorConstructorOptimization", { ++ itBundled("minify/ErrorConstructorPositions", { + files: { + "/entry.js": /* js */ ` + // Test all Error constructors +@@ -1272,25 +1272,25 @@ describe("bundler", () => { + `, + }, + capture: [ +- "Error()", +- 'Error("message")', +- 'Error("message", { cause: "cause" })', +- "TypeError()", +- 'TypeError("type error")', +- "SyntaxError()", +- 'SyntaxError("syntax error")', +- "RangeError()", +- 'RangeError("range error")', +- "ReferenceError()", +- 'ReferenceError("ref error")', +- "EvalError()", +- 'EvalError("eval error")', +- "URIError()", +- 'URIError("uri error")', +- 'AggregateError([], "aggregate error")', +- 'AggregateError([Error("e1")], "multiple")', +- "Error(msg)", +- "TypeError(getErrorMessage())", ++ "new Error", ++ 'new Error("message")', ++ 'new Error("message", { cause: "cause" })', ++ "new TypeError", ++ 'new TypeError("type error")', ++ "new SyntaxError", ++ 'new SyntaxError("syntax error")', ++ "new RangeError", ++ 'new RangeError("range error")', ++ "new ReferenceError", ++ 'new ReferenceError("ref error")', ++ "new EvalError", ++ 'new EvalError("eval error")', ++ "new URIError", ++ 'new URIError("uri error")', ++ 'new AggregateError([], "aggregate error")', ++ 'new AggregateError([new Error("e1")], "multiple")', ++ "new Error(msg)", ++ "new TypeError(getErrorMessage())", + "/* @__PURE__ */ new Date", + "/* @__PURE__ */ new Map", + "/* @__PURE__ */ new Set", +diff --git a/test/js/bun/jsc/webkit-upgrade-7b485a76e9.test.ts b/test/js/bun/jsc/webkit-upgrade-7b485a76e9.test.ts +index 27805cb4..42f6196b 100644 +--- a/test/js/bun/jsc/webkit-upgrade-7b485a76e9.test.ts ++++ b/test/js/bun/jsc/webkit-upgrade-7b485a76e9.test.ts +@@ -150,8 +150,8 @@ describe("WebKit 7b485a76e9 upgrade", () => { + const frames = [...stack.matchAll(/positions\.js:(\d+):(\d+)/g)].map(m => [Number(m[1]), Number(m[2])]); + expect(frames).toEqual([ + [6, 28], +- [4, 15], +- [7, 6], ++ [4, 10], ++ [7, 1], + ]); + + // lineOffset and columnOffset move the first line. columnOffset does not move later lines. +diff --git a/test/js/bun/sourcemap/internal-sourcemap-roundtrip.test.ts b/test/js/bun/sourcemap/internal-sourcemap-roundtrip.test.ts +index ba26e511..e5d1ebd8 100644 +--- a/test/js/bun/sourcemap/internal-sourcemap-roundtrip.test.ts ++++ b/test/js/bun/sourcemap/internal-sourcemap-roundtrip.test.ts +@@ -486,7 +486,7 @@ describe.concurrent("sourcemap of a source with a truncated trailing UTF-8 seque + const map = await Bun.file(path.join(String(dir), "out", "in.js.map")).json(); + expect(map).toMatchObject({ + sources: ["../in.js"], +- mappings: ";AAAA,QAAQ,IAAI,CAAC;", ++ mappings: ";AAAA,QAAQ,GAAG,CAAC,CAAC;", + }); + }); + }); +diff --git a/test/js/bun/sourcemap/internal-sourcemap.test.ts b/test/js/bun/sourcemap/internal-sourcemap.test.ts +index b8efdae7..63c78056 100644 +--- a/test/js/bun/sourcemap/internal-sourcemap.test.ts ++++ b/test/js/bun/sourcemap/internal-sourcemap.test.ts +@@ -76,9 +76,9 @@ describe("InternalSourceMap", () => { + + const positions = extractPositions(stdout); + expect(positions).toEqual([ +- "6:13", // throw new Error("boom") ++ "6:9", // throw new Error("boom") + "24:3", // gamma() at top level +- "31:17", // new Error("here").stack ++ "31:13", // new Error("here").stack + "34:13", // captureViaCaptureStackTrace() + ]); + +@@ -103,10 +103,12 @@ describe("InternalSourceMap", () => { + const { stdout, stderr, exited } = await run({ "index.ts": lines.join("\n") + "\n" }); + + expect(stderr).toBe(""); +- // Each frame must point at its line's `Error` in 1-based UTF-16 columns, ++ // Each frame must point at its line's `new` keyword in 1-based UTF-16 columns, + // `a1` being the all-ASCII control. + const frames = [...stdout.matchAll(/at ([a-d]1) \(.*index\.ts:(\d+):(\d+)\)/g)].map(m => `${m[1]} ${m[2]}:${m[3]}`); +- expect(frames).toEqual(["a1", "b1", "c1", "d1"].map((fn, i) => `${fn} ${i + 1}:${lines[i].indexOf("Error(") + 1}`)); ++ expect(frames).toEqual( ++ ["a1", "b1", "c1", "d1"].map((fn, i) => `${fn} ${i + 1}:${lines[i].indexOf("new Error(") + 1}`), ++ ); + expect(exited).toBe(0); + }); + +diff --git a/test/js/node/module/node-module-module.test.js b/test/js/node/module/node-module-module.test.js +index a5bc03c0..2bee38b3 100644 +--- a/test/js/node/module/node-module-module.test.js ++++ b/test/js/node/module/node-module-module.test.js +@@ -1781,7 +1781,7 @@ console.log("survived", require("./late.js"));`, + expect(stderr).toMatchInlineSnapshot(` + "1 | require("module").runMain = () => { + 2 | throw new RangeError("from the override"); +- ^ ++ ^ + RangeError: from the override + at (file:NN:NN) + +diff --git a/test/js/node/v8/callsite-async-position-fixture.mjs b/test/js/node/v8/callsite-async-position-fixture.mjs +new file mode 100644 +index 00000000..84cfc6db +--- /dev/null ++++ b/test/js/node/v8/callsite-async-position-fixture.mjs +@@ -0,0 +1,12 @@ ++async function leaf() { ++await Promise.resolve(); ++return new Error().stack; ++} ++async function parent() { ++return await leaf(); ++} ++const trace = await parent(); ++const parentFrame = trace.split("\n").find(line => /\bparent(?:\s|@|\()/.test(line)); ++const position = /:(\d+):(\d+)\)?$/.exec(parentFrame); ++if (!position || +position[1] !== 6 || +position[2] !== 8) ++ throw new Error(JSON.stringify({ expected: [6, 8], trace })); +diff --git a/test/js/node/v8/callsite-syntax-positions-fixture.js b/test/js/node/v8/callsite-syntax-positions-fixture.js +new file mode 100644 +index 00000000..32eed3ee +--- /dev/null ++++ b/test/js/node/v8/callsite-syntax-positions-fixture.js +@@ -0,0 +1,116 @@ ++// Keep call positions independent of exception-expression divots. ++let trace; ++function capture() { const stack = new Error().stack; trace = stack; return stack; } ++function Capture() { this.stack = new Error().stack; trace = this.stack; } ++function factory() { return capture; } ++const object = { capture }; ++const name = "capture"; ++const cases = [ ++ [function () { ++capture() ++ }, 10, 1, "capture()"], ++ [function () { ++capture /* comment */ () ++ }, 13, 1, "capture /* comment */ ()"], ++ [function () { ++capture ++() ++ }, 16, 1, "capture\n()"], ++ [function () { ++factory()() ++ }, 20, 10, "factory()()"], ++ [function () { ++factory() ++() ++ }, 24, 1, "factory()\n()"], ++ [function () { ++factory() /* comment */ () ++ }, 27, 25, "factory() /* comment */ ()"], ++ [function () { ++object.capture() ++ }, 30, 8, "object.capture()"], ++ [function () { ++object ++.capture() ++ }, 34, 2, "object\n.capture()"], ++ [function () { ++object . capture() ++ }, 37, 10, "object . capture()"], ++ [function () { ++object. /* comment */ capture() ++ }, 40, 23, "object. /* comment */ capture()"], ++ [function () { ++object.\u0063apture() ++ }, 43, 8, "object.\\u0063apture()"], ++ [function () { ++object[name]() ++ }, 46, 13, "object[name]()"], ++ [function () { ++object["capture"]() ++ }, 49, 18, "object[\"capture\"]()"], ++ [function () { ++object[ ++name ++] ++() ++ }, 55, 1, "object[\nname\n]\n()"], ++ [function () { ++(capture)() ++ }, 58, 10, "(capture)()"], ++ [function () { ++((capture))() ++ }, 61, 12, "((capture))()"], ++ [function () { ++(object.capture)() ++ }, 64, 17, "(object.capture)()"], ++ [function () { ++(object[name])() ++ }, 67, 15, "(object[name])()"], ++ [function () { ++(0, capture)() ++ }, 70, 13, "(0, capture)()"], ++ [function () { ++capture?.() ++ }, 73, 10, "capture?.()"], ++ [function () { ++object?.capture() ++ }, 76, 9, "object?.capture()"], ++ [function () { ++object.capture?.() ++ }, 79, 17, "object.capture?.()"], ++ [function () { ++object?.capture?.() ++ }, 82, 18, "object?.capture?.()"], ++ [function () { ++this() ++ }, 85, 5, "this()"], ++ [function () { ++capture`text` ++ }, 88, 8, "capture`text`"], ++ [function () { ++Reflect.apply(capture, null, []) ++ }, 91, 9, "Reflect.apply(capture, null, [])"], ++ [function () { ++new Capture().stack ++ }, 94, 1, "new Capture().stack"], ++ [function () { ++new ++Capture().stack ++ }, 97, 1, "new\nCapture().stack"], ++ [function () { ++new (Capture)().stack ++ }, 101, 1, "new (Capture)().stack"], ++ [function () { ++Reflect.construct(Capture, []).stack ++ }, 104, 9, "Reflect.construct(Capture, []).stack"], ++]; ++for (let iteration = 0; iteration < Math.min(typeof testLoopCount === "number" ? testLoopCount : 20, 100); ++iteration) { ++ for (const [fn, line, column, body] of cases) { ++ trace = ""; ++ fn.call(capture); ++ const frames = trace.split("\n").filter(line => /:(\d+):(\d+)\)?$/.test(line)); ++ const position = /:(\d+):(\d+)\)?$/.exec(frames[1]); ++ if (!position || +position[1] !== line || +position[2] !== column) ++ throw new Error(JSON.stringify({ body, expected: [line, column], trace })); ++ } ++} +diff --git a/test/js/node/v8/callsite-typescript-positions-fixture.ts b/test/js/node/v8/callsite-typescript-positions-fixture.ts +new file mode 100644 +index 00000000..1bc876b8 +--- /dev/null ++++ b/test/js/node/v8/callsite-typescript-positions-fixture.ts +@@ -0,0 +1,63 @@ ++import assert from "node:assert/strict"; ++let frame: NodeJS.CallSite; ++function capture(..._args: unknown[]) { ++ const target: { stack?: NodeJS.CallSite[] } = {}; ++ const previous = Error.prepareStackTrace; ++ try { ++ Error.prepareStackTrace = (_, frames) => frames; ++ Error.captureStackTrace(target, capture); ++ frame = target.stack![0]; ++ } finally { ++ Error.prepareStackTrace = previous; ++ } ++} ++const object = { capture }; ++const cases = [ ++ [function () { ++(object).capture(); ++ }, 17, 10, "receiver-parentheses"], ++ [function () { ++(object.capture)(); ++ }, 20, 25, "callee-parentheses"], ++ [function () { ++((capture))(); ++ }, 23, 20, "nested-parentheses"], ++ [function () { ++(capture as typeof capture)(); ++ }, 26, 36, "asserted-callee"], ++ [function () { ++object["capture"](); ++ }, 29, 26, "computed-callee"], ++ [function () { ++object?.["capture"](); ++ }, 32, 28, "optional-computed-callee"], ++ [function () { ++const marker = "🎉"; (capture)(); ++ }, 35, 39, "astral-prefix"], ++ [function () { ++(capture); object.capture(); ++ }, 38, 19, "prior-parenthesized-expression"], ++ [function () { ++(capture)<() => T>(); ++ }, 41, 22, "generic-function-type"], ++ [function () { ++(capture)!(); ++ }, 44, 11, "non-null-callee"], ++ [function () { ++(capture)!(); ++ }, 47, 19, "generic-non-null-callee"], ++ [function () { ++(capture)!!(); ++ }, 50, 12, "repeated-non-null-callee"], ++ [function () { ++(object)!.capture(); ++ }, 53, 11, "non-null-receiver"], ++ [function () { ++capture`text`; ++ }, 56, 16, "tagged-template"], ++ ] as const; ++for (const [fn, line, column, label] of cases) { ++ fn(); ++ assert.deepEqual([frame!.getLineNumber(), frame!.getColumnNumber()], [line, column], label); ++ assert.deepEqual(/:(\d+):(\d+)\)?$/.exec(frame!.toString())!.slice(1).map(Number), [line, column], label); ++} +diff --git a/test/js/node/v8/capture-stack-trace.test.js b/test/js/node/v8/capture-stack-trace.test.js +index ac527424..fa239c56 100644 +--- a/test/js/node/v8/capture-stack-trace.test.js ++++ b/test/js/node/v8/capture-stack-trace.test.js +@@ -206,8 +206,8 @@ test.concurrent("CallSite columns preserve async continuation metadata", async ( + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual([ + { position: [4, 7], textPosition: [4, 7], async: false, eval: false, native: false, file: true }, +- // JSC selects the awaited call; V8 selects `await`. Preserve the location while fixing its units. +- { position: [8, 14], textPosition: [8, 14], async: true, eval: false, native: false, file: true }, ++ // Async frames point at `await`, as in Node. ++ { position: [8, 8], textPosition: [8, 8], async: true, eval: false, native: false, file: true }, + ]); + expect(exitCode).toBe(0); + }); +@@ -2091,7 +2091,7 @@ test.concurrent( + "define stack": error => (Object.defineProperty(error, "stack", { value: "defined" }), error.stack), + // A clone is made from the complete string: it must not get a second first line. + "structuredClone": error => structuredClone(error).stack.split("\\n").filter(line => line.includes("boom")), +- "Bun.inspect": error => Bun.inspect(error).split("\\n").filter(line => line.includes("boom")).map(line => line.trim()), ++ "Bun.inspect": error => Bun.inspect(error).split("\\n").map(line => line.trim()).filter(line => line === "TypeError: boom"), + "JSON.stringify": error => JSON.stringify(error), + }; + const rows = {}; +@@ -2412,7 +2412,7 @@ test.concurrent.each([[{}], [{ BUN_JSC_useSourceProviderCache: "0" }]])( + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); +- expect(stdout.trim()).toEndWith("[eval]:5:14)"); // 2:18 when the lexer resumed on the template literal's first line ++ expect(stdout.trim()).toEndWith("[eval]:5:10)"); // Line 2 when the lexer resumed on the template literal's first line + expect(exitCode).toBe(0); + }, + ); +@@ -2456,3 +2456,31 @@ test.skipIf(totalmem() < 10 * 1024 ** 3)( + }, + 30_000, + ); ++ ++test("syntax positions preserve built-in constructors, arrow frames, and property reads", async () => { ++ await using proc = Bun.spawn({ ++ cmd: [bunExe(), import.meta.dir + "/stack-position-regressions-fixture.js"], ++ env: bunEnv, ++ stdout: "pipe", ++ stderr: "pipe", ++ }); ++ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); ++ expect({ stdout, stderr, exitCode }).toEqual({ stdout: "", stderr: "", exitCode: 0 }); ++}); ++ ++for (const fixture of [ ++ "callsite-syntax-positions-fixture.js", ++ "callsite-async-position-fixture.mjs", ++ "callsite-typescript-positions-fixture.ts", ++]) { ++ test(`syntax-selected stack positions: ${fixture}`, async () => { ++ await using proc = Bun.spawn({ ++ cmd: [bunExe(), import.meta.dir + "/" + fixture], ++ env: bunEnv, ++ stdout: "pipe", ++ stderr: "pipe", ++ }); ++ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); ++ expect({ stdout, stderr, exitCode }).toEqual({ stdout: "", stderr: "", exitCode: 0 }); ++ }); ++} +diff --git a/test/js/node/v8/error-prepare-stack-default-fixture.js b/test/js/node/v8/error-prepare-stack-default-fixture.js +index 17df9c6d..5e3ce31b 100644 +--- a/test/js/node/v8/error-prepare-stack-default-fixture.js ++++ b/test/js/node/v8/error-prepare-stack-default-fixture.js +@@ -30,8 +30,8 @@ function oneWrapperLevel() { + oneWrapperLevel(); + + // The native line column numbers might differ a bit here. +-const stackIgnoringLineAndColumn = stack.replaceAll(":12:26", ":NN:NN").replaceAll(/native:.*$/gm, "native)"); +-const stack2IgnoringLineAndColumn = stack2.replaceAll(":17:26", ":NN:NN").replaceAll(/native:.*$/gm, "native)"); ++const stackIgnoringLineAndColumn = stack.replaceAll(":12:9", ":NN:NN").replaceAll(/native:.*$/gm, "native)"); ++const stack2IgnoringLineAndColumn = stack2.replaceAll(":17:9", ":NN:NN").replaceAll(/native:.*$/gm, "native)"); + if (stackIgnoringLineAndColumn !== stack2IgnoringLineAndColumn) { + console.log("\n-----\n"); + console.log(stackIgnoringLineAndColumn); +diff --git a/test/js/node/v8/stack-position-regressions-fixture.js b/test/js/node/v8/stack-position-regressions-fixture.js +new file mode 100644 +index 00000000..fe71747d +--- /dev/null ++++ b/test/js/node/v8/stack-position-regressions-fixture.js +@@ -0,0 +1,104 @@ ++"use strict"; ++const name = "x"; ++const cases = [ ++ ['Error', function () { ++const error = new Error("test"); return error; ++ }, 5, 15], ++ ['TypeError', function () { ++const error = new TypeError("test"); return error; ++ }, 8, 15], ++ ['SyntaxError', function () { ++const error = new SyntaxError("test"); return error; ++ }, 11, 15], ++ ['RangeError', function () { ++const error = new RangeError("test"); return error; ++ }, 14, 15], ++ ['ReferenceError', function () { ++const error = new ReferenceError("test"); return error; ++ }, 17, 15], ++ ['EvalError', function () { ++const error = new EvalError("test"); return error; ++ }, 20, 15], ++ ['URIError', function () { ++const error = new URIError("test"); return error; ++ }, 23, 15], ++ ['AggregateError', function () { ++const error = new AggregateError([], "test"); return error; ++ }, 26, 15], ++ ['arrow-new', () => new Error("arrow"), 28, 25], ++ ['arrow-type', () => new TypeError("arrow"), 29, 26], ++ ['arrow-alias', () => new globalThis.Error("arrow"), 30, 27], ++ ['multiline-new', function () { ++const error = new ++ Error("multiline"); return error; ++ }, 32, 15], ++ ['constructor-comment', function () { ++const error = new /* comment */ Error("comment"); return error; ++ }, 36, 15], ++ ['null-dot', function () { ++try { null.x; } catch (error) { return error; } ++ }, 39, 12], ++ ['undefined-dot', function () { ++try { undefined.x; } catch (error) { return error; } ++ }, 42, 17], ++ ['null-length', function () { ++try { null.length; } catch (error) { return error; } ++ }, 45, 12], ++ ['null-comment', function () { ++try { null . /* comment */ x; } catch (error) { return error; } ++ }, 48, 28], ++ ['null-escape', function () { ++try { null.\u0078; } catch (error) { return error; } ++ }, 51, 12], ++ ['null-multiline', function () { ++try { null ++ .x; } catch (error) { return error; } ++ }, 55, 6], ++ ['null-bracket', function () { ++try { null["x"]; } catch (error) { return error; } ++ }, 58, 11], ++ ['null-number', function () { ++try { null[1]; } catch (error) { return error; } ++ }, 61, 11], ++ ['null-computed', function () { ++try { null[name]; } catch (error) { return error; } ++ }, 64, 11], ++ ['null-call', function () { ++try { null.x(); } catch (error) { return error; } ++ }, 67, 12], ++ ['null-parenthesized-call', function () { ++try { (null.x)(); } catch (error) { return error; } ++ }, 70, 13], ++ ['null-bracket-call', function () { ++try { null["x"](); } catch (error) { return error; } ++ }, 73, 11], ++ ['null-call-method', function () { ++try { null.call(); } catch (error) { return error; } ++ }, 76, 12], ++ ['null-apply-method', function () { ++try { null.apply(null, []); } catch (error) { return error; } ++ }, 79, 12], ++ ['null-has-own', function () { ++try { null.hasOwnProperty(name); } catch (error) { return error; } ++ }, 82, 12], ++ ]; ++function checkPosition(error, line, column, shape, structured) { ++ const frames = structured ? error.stack : error.stack.split("\n").filter(frame => /:(\d+):(\d+)\)?$/.test(frame)); ++ const actual = structured ? [frames[0].getLineNumber(), frames[0].getColumnNumber()] : /:(\d+):(\d+)\)?$/.exec(frames[0])?.slice(1).map(Number); ++ if (!actual || actual[0] !== line || actual[1] !== column) ++ throw new Error(JSON.stringify({ shape, structured, expected: [line, column], actual, trace: structured ? frames.map(String) : error.stack })); ++} ++for (let iteration = 0; iteration < Math.min(typeof testLoopCount === "number" ? testLoopCount : 20, 100); ++iteration) { ++ for (const [shape, create, line, column] of cases) { ++ checkPosition(create(), line, column, shape, false); ++ if (typeof process !== "undefined") { ++ const previous = Error.prepareStackTrace; ++ try { ++ Error.prepareStackTrace = (_, frames) => frames; ++ checkPosition(create(), line, column, shape, true); ++ } finally { ++ Error.prepareStackTrace = previous; ++ } ++ } ++ } ++} +diff --git a/test/js/node/vm/__snapshots__/vm-sourceUrl.test.ts.snap b/test/js/node/vm/__snapshots__/vm-sourceUrl.test.ts.snap +index bbc85154..43f86730 100644 +--- a/test/js/node/vm/__snapshots__/vm-sourceUrl.test.ts.snap ++++ b/test/js/node/vm/__snapshots__/vm-sourceUrl.test.ts.snap +@@ -16,7 +16,7 @@ exports[`can get sourceURL inside node:vm 1`] = ` + ^ + error: hello + at hello (hellohello.js:4:24) +- at hellohello.js:7:6 ++ at hellohello.js:7:1 + at (:21:15) + " + `; +@@ -26,7 +26,7 @@ exports[`eval sourceURL is correct 1`] = ` + ^ + error: hello + at hello (hellohello.js:4:24) +- at eval (hellohello.js:7:6) ++ at eval (hellohello.js:7:1) + at (:39:15) + " + `; diff --git a/.github/openclaw/qualification/stack-visitor.js b/.github/openclaw/qualification/stack-visitor.js new file mode 100644 index 0000000000000..bf5358a299ad7 --- /dev/null +++ b/.github/openclaw/qualification/stack-visitor.js @@ -0,0 +1,3 @@ +function witness() { jscStack(); } +function parent() { witness(); } +parent(); diff --git a/.github/openclaw/qualify.sh b/.github/openclaw/qualify.sh index fb7fb33a22832..d4dcea1676622 100644 --- a/.github/openclaw/qualify.sh +++ b/.github/openclaw/qualify.sh @@ -27,6 +27,7 @@ for arm in baseline candidate; do git apply --exclude=CHANGELOG.md --exclude='docs/*' --exclude='test/js/node/worker_threads/*' "$patch" done git apply --exclude=CHANGELOG.md --exclude='docs/*' "$inputs/patches/008-namespace.patch" + git apply --exclude=CHANGELOG.md --exclude='docs/*' "$inputs/patches/009-stack-positions.patch" cp "$inputs/worker-resource-limits.test.ts" test/js/node/worker_threads/openclaw-resource-limits.test.ts git diff --check git diff --binary HEAD > "$QUALIFICATION_DIR/bun-adapters.patch" @@ -60,6 +61,7 @@ python3 "$inputs/compare-fork-ci.py" "$QUALIFICATION_DIR/baseline" "$QUALIFICATI candidate="$QUALIFICATION_DIR/bun/build/qualify-candidate/bun" unset BUN_WEBKIT_ARTIFACT_MANIFEST BUN_BUILD_CACHE_DIR BUN_BUILD_PREFETCH_DIR "$candidate" test test/js/bun/resolve/esModule.test.ts test/js/bun/namespace-prototype-pollution.test.ts test/js/bun/resolve/require-esm-evaluating-cycle.test.ts test/js/bun/import-attributes/import-attributes.test.ts test/js/node/module/require-extensions.test.ts > "$QUALIFICATION_DIR/namespace.log" 2>&1 +"$candidate" test --expose-internals test/js/node/v8/capture-stack-trace.test.js test/js/bun/sourcemap/internal-sourcemap.test.ts > "$QUALIFICATION_DIR/stack-positions.log" 2>&1 "$candidate" "$inputs/segmenter.js" > "$QUALIFICATION_DIR/segmenter.log" 2>&1 "$candidate" test test/js/node/vm/vm.test.ts -t 'a Proxy in the prototype chain' > "$QUALIFICATION_DIR/proxy.log" 2>&1 "$candidate" test test/js/node/worker_threads/openclaw-resource-limits.test.ts -t 'resourceLimits' > "$QUALIFICATION_DIR/resource-limits.log" 2>&1 diff --git a/.github/openclaw/test_verify_jsc_results.py b/.github/openclaw/test_verify_jsc_results.py new file mode 100644 index 0000000000000..6c2e7b911aab6 --- /dev/null +++ b/.github/openclaw/test_verify_jsc_results.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).with_name("verify-jsc-results.py") + + +class JSCResultTests(unittest.TestCase): + def run_check(self, rows, *args): + with tempfile.TemporaryDirectory() as directory: + (Path(directory) / "results").write_text("\n".join(rows)) + return subprocess.run([sys.executable, str(SCRIPT), directory, *args], capture_output=True, text=True) + + def test_default_requires_namespace(self): + self.assertNotEqual(self.run_check(["stress/another.js.default: PASS"]).returncode, 0) + result = self.run_check(["stress/module-namespace-esmodule-marker.js.default: PASS"]) + self.assertEqual(result.returncode, 0, result.stderr) + + def test_all_requested_regressions_must_execute(self): + result = self.run_check(["stress/callsite.js.default: PASS"], "--require", "callsite.js", "--require", "property.js") + self.assertNotEqual(result.returncode, 0) + self.assertIn("property.js", result.stderr) + + def test_any_failed_configuration_rejects_results(self): + result = self.run_check(["stress/callsite.js.default: PASS", "stress/callsite.js.ftl-eager: FAIL"], "--require", "callsite.js") + self.assertNotEqual(result.returncode, 0) + self.assertIn("ftl-eager: FAIL", result.stderr) + self.assertNotEqual(self.run_check([], "--require", "callsite.js").returncode, 0) + + def test_reports_all_passing_variants(self): + result = self.run_check(["stress/callsite.js.default: PASS", "stress/callsite.js.ftl-eager: PASS", "stress/property.js.default: PASS"], "--require", "callsite.js", "--require", "property.js") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout), {"tests": 3, "required_variants": {"callsite.js": 2, "property.js": 1}, "passed": True}) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/openclaw/verify-jsc-results.py b/.github/openclaw/verify-jsc-results.py index e6bfcea1bb07b..9fa754c51001a 100644 --- a/.github/openclaw/verify-jsc-results.py +++ b/.github/openclaw/verify-jsc-results.py @@ -1,17 +1,23 @@ #!/usr/bin/env python3 """The stress runner's results file, not its exit status, is authoritative.""" +import argparse import json -import sys from pathlib import Path -root = Path(sys.argv[1]) +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("root", type=Path) +parser.add_argument("--require", action="append", dest="required", help="Regression filename that must execute (repeatable)") +args = parser.parse_args() +root = args.root lines = (root / "results").read_text().splitlines() if not lines: raise SystemExit("JSC produced no test results") -namespace = [line for line in lines if "module-namespace-esmodule-marker.js" in line] -if not namespace: - raise SystemExit("The namespace marker regression did not execute") +required = args.required or ["module-namespace-esmodule-marker.js"] +variants = {name: sum(name in line for line in lines) for name in required} +missing = [name for name, count in variants.items() if not count] +if missing: + raise SystemExit("JSC regressions did not execute: " + ", ".join(missing)) failures = [line for line in lines if not line.endswith(": PASS")] if failures: raise SystemExit("JSC failures:\n" + "\n".join(failures)) -print(json.dumps({"tests": len(lines), "marker_variants": len(namespace), "passed": True})) +print(json.dumps({"tests": len(lines), "required_variants": variants, "passed": True})) diff --git a/.github/workflows/openclaw-checks.yml b/.github/workflows/openclaw-checks.yml index d41891ab71558..472599f9e028c 100644 --- a/.github/workflows/openclaw-checks.yml +++ b/.github/workflows/openclaw-checks.yml @@ -34,6 +34,7 @@ jobs: - name: Verify pipeline integrity and runner support run: | python3 .github/openclaw/test_release.py + python3 .github/openclaw/test_verify_jsc_results.py bash .github/openclaw/preflight.sh - name: Build native Linux engine with the unchanged lane env: @@ -60,6 +61,17 @@ jobs: "$RUNNER_TEMP/jsc-artifact/bun-webkit/bin/jsc" --useDollarVM=1 -e 'if (typeof $vm.moduleNamespaceForRequire !== "function") throw new Error("namespace test hook is missing");' Tools/Scripts/run-jsc-stress-tests JSTests/stress --jsc "$RUNNER_TEMP/jsc-artifact/bun-webkit/bin/jsc" --child-processes 16 --filter 'module-namespace|module-loader-security-thenable-namespace' --output-dir "$RUNNER_TEMP/jsc-results" --no-retry python3 .github/openclaw/verify-jsc-results.py "$RUNNER_TEMP/jsc-results" + - name: Exercise syntax-selected stack positions + shell: bash + run: | + set -euo pipefail + jsc="$RUNNER_TEMP/jsc-artifact/bun-webkit/bin/jsc" + Tools/Scripts/run-jsc-stress-tests JSTests/stress --jsc "$jsc" --child-processes 16 --filter 'callsite-syntax-positions|stack-position-regressions' --output-dir "$RUNNER_TEMP/stack-results" --no-retry + python3 .github/openclaw/verify-jsc-results.py "$RUNNER_TEMP/stack-results" --require callsite-syntax-positions.js --require stack-position-regressions.js + python3 .github/openclaw/check-stack-visitor.py "$jsc" + "$jsc" -m JSTests/stress/callsite-async-position.mjs + "$jsc" --useJIT=false -m JSTests/stress/callsite-async-position.mjs + "$jsc" --useBytecodeOptimizer=true -m JSTests/stress/callsite-async-position.mjs - name: Exercise allocation sampling across execution tiers shell: bash run: | @@ -91,5 +103,7 @@ jobs: ${{ runner.temp }}/qualification/candidate/*.log ${{ runner.temp }}/jsc-results/results ${{ runner.temp }}/jsc-results/*.log + ${{ runner.temp }}/stack-results/results + ${{ runner.temp }}/stack-results/*.log if-no-files-found: error retention-days: 7 diff --git a/JSTests/stress/callsite-async-position.mjs b/JSTests/stress/callsite-async-position.mjs new file mode 100644 index 0000000000000..84cfc6db79100 --- /dev/null +++ b/JSTests/stress/callsite-async-position.mjs @@ -0,0 +1,12 @@ +async function leaf() { +await Promise.resolve(); +return new Error().stack; +} +async function parent() { +return await leaf(); +} +const trace = await parent(); +const parentFrame = trace.split("\n").find(line => /\bparent(?:\s|@|\()/.test(line)); +const position = /:(\d+):(\d+)\)?$/.exec(parentFrame); +if (!position || +position[1] !== 6 || +position[2] !== 8) + throw new Error(JSON.stringify({ expected: [6, 8], trace })); diff --git a/JSTests/stress/callsite-syntax-positions.js b/JSTests/stress/callsite-syntax-positions.js new file mode 100644 index 0000000000000..32eed3eed0dbc --- /dev/null +++ b/JSTests/stress/callsite-syntax-positions.js @@ -0,0 +1,116 @@ +// Keep call positions independent of exception-expression divots. +let trace; +function capture() { const stack = new Error().stack; trace = stack; return stack; } +function Capture() { this.stack = new Error().stack; trace = this.stack; } +function factory() { return capture; } +const object = { capture }; +const name = "capture"; +const cases = [ + [function () { +capture() + }, 10, 1, "capture()"], + [function () { +capture /* comment */ () + }, 13, 1, "capture /* comment */ ()"], + [function () { +capture +() + }, 16, 1, "capture\n()"], + [function () { +factory()() + }, 20, 10, "factory()()"], + [function () { +factory() +() + }, 24, 1, "factory()\n()"], + [function () { +factory() /* comment */ () + }, 27, 25, "factory() /* comment */ ()"], + [function () { +object.capture() + }, 30, 8, "object.capture()"], + [function () { +object +.capture() + }, 34, 2, "object\n.capture()"], + [function () { +object . capture() + }, 37, 10, "object . capture()"], + [function () { +object. /* comment */ capture() + }, 40, 23, "object. /* comment */ capture()"], + [function () { +object.\u0063apture() + }, 43, 8, "object.\\u0063apture()"], + [function () { +object[name]() + }, 46, 13, "object[name]()"], + [function () { +object["capture"]() + }, 49, 18, "object[\"capture\"]()"], + [function () { +object[ +name +] +() + }, 55, 1, "object[\nname\n]\n()"], + [function () { +(capture)() + }, 58, 10, "(capture)()"], + [function () { +((capture))() + }, 61, 12, "((capture))()"], + [function () { +(object.capture)() + }, 64, 17, "(object.capture)()"], + [function () { +(object[name])() + }, 67, 15, "(object[name])()"], + [function () { +(0, capture)() + }, 70, 13, "(0, capture)()"], + [function () { +capture?.() + }, 73, 10, "capture?.()"], + [function () { +object?.capture() + }, 76, 9, "object?.capture()"], + [function () { +object.capture?.() + }, 79, 17, "object.capture?.()"], + [function () { +object?.capture?.() + }, 82, 18, "object?.capture?.()"], + [function () { +this() + }, 85, 5, "this()"], + [function () { +capture`text` + }, 88, 8, "capture`text`"], + [function () { +Reflect.apply(capture, null, []) + }, 91, 9, "Reflect.apply(capture, null, [])"], + [function () { +new Capture().stack + }, 94, 1, "new Capture().stack"], + [function () { +new +Capture().stack + }, 97, 1, "new\nCapture().stack"], + [function () { +new (Capture)().stack + }, 101, 1, "new (Capture)().stack"], + [function () { +Reflect.construct(Capture, []).stack + }, 104, 9, "Reflect.construct(Capture, []).stack"], +]; +for (let iteration = 0; iteration < Math.min(typeof testLoopCount === "number" ? testLoopCount : 20, 100); ++iteration) { + for (const [fn, line, column, body] of cases) { + trace = ""; + fn.call(capture); + const frames = trace.split("\n").filter(line => /:(\d+):(\d+)\)?$/.test(line)); + const position = /:(\d+):(\d+)\)?$/.exec(frames[1]); + if (!position || +position[1] !== line || +position[2] !== column) + throw new Error(JSON.stringify({ body, expected: [line, column], trace })); + } +} diff --git a/JSTests/stress/stack-position-regressions.js b/JSTests/stress/stack-position-regressions.js new file mode 100644 index 0000000000000..fe71747d9f95d --- /dev/null +++ b/JSTests/stress/stack-position-regressions.js @@ -0,0 +1,104 @@ +"use strict"; +const name = "x"; +const cases = [ + ['Error', function () { +const error = new Error("test"); return error; + }, 5, 15], + ['TypeError', function () { +const error = new TypeError("test"); return error; + }, 8, 15], + ['SyntaxError', function () { +const error = new SyntaxError("test"); return error; + }, 11, 15], + ['RangeError', function () { +const error = new RangeError("test"); return error; + }, 14, 15], + ['ReferenceError', function () { +const error = new ReferenceError("test"); return error; + }, 17, 15], + ['EvalError', function () { +const error = new EvalError("test"); return error; + }, 20, 15], + ['URIError', function () { +const error = new URIError("test"); return error; + }, 23, 15], + ['AggregateError', function () { +const error = new AggregateError([], "test"); return error; + }, 26, 15], + ['arrow-new', () => new Error("arrow"), 28, 25], + ['arrow-type', () => new TypeError("arrow"), 29, 26], + ['arrow-alias', () => new globalThis.Error("arrow"), 30, 27], + ['multiline-new', function () { +const error = new + Error("multiline"); return error; + }, 32, 15], + ['constructor-comment', function () { +const error = new /* comment */ Error("comment"); return error; + }, 36, 15], + ['null-dot', function () { +try { null.x; } catch (error) { return error; } + }, 39, 12], + ['undefined-dot', function () { +try { undefined.x; } catch (error) { return error; } + }, 42, 17], + ['null-length', function () { +try { null.length; } catch (error) { return error; } + }, 45, 12], + ['null-comment', function () { +try { null . /* comment */ x; } catch (error) { return error; } + }, 48, 28], + ['null-escape', function () { +try { null.\u0078; } catch (error) { return error; } + }, 51, 12], + ['null-multiline', function () { +try { null + .x; } catch (error) { return error; } + }, 55, 6], + ['null-bracket', function () { +try { null["x"]; } catch (error) { return error; } + }, 58, 11], + ['null-number', function () { +try { null[1]; } catch (error) { return error; } + }, 61, 11], + ['null-computed', function () { +try { null[name]; } catch (error) { return error; } + }, 64, 11], + ['null-call', function () { +try { null.x(); } catch (error) { return error; } + }, 67, 12], + ['null-parenthesized-call', function () { +try { (null.x)(); } catch (error) { return error; } + }, 70, 13], + ['null-bracket-call', function () { +try { null["x"](); } catch (error) { return error; } + }, 73, 11], + ['null-call-method', function () { +try { null.call(); } catch (error) { return error; } + }, 76, 12], + ['null-apply-method', function () { +try { null.apply(null, []); } catch (error) { return error; } + }, 79, 12], + ['null-has-own', function () { +try { null.hasOwnProperty(name); } catch (error) { return error; } + }, 82, 12], + ]; +function checkPosition(error, line, column, shape, structured) { + const frames = structured ? error.stack : error.stack.split("\n").filter(frame => /:(\d+):(\d+)\)?$/.test(frame)); + const actual = structured ? [frames[0].getLineNumber(), frames[0].getColumnNumber()] : /:(\d+):(\d+)\)?$/.exec(frames[0])?.slice(1).map(Number); + if (!actual || actual[0] !== line || actual[1] !== column) + throw new Error(JSON.stringify({ shape, structured, expected: [line, column], actual, trace: structured ? frames.map(String) : error.stack })); +} +for (let iteration = 0; iteration < Math.min(typeof testLoopCount === "number" ? testLoopCount : 20, 100); ++iteration) { + for (const [shape, create, line, column] of cases) { + checkPosition(create(), line, column, shape, false); + if (typeof process !== "undefined") { + const previous = Error.prepareStackTrace; + try { + Error.prepareStackTrace = (_, frames) => frames; + checkPosition(create(), line, column, shape, true); + } finally { + Error.prepareStackTrace = previous; + } + } + } +} diff --git a/OPENCLAW.md b/OPENCLAW.md index 126c4c51e848e..1b35ae8a3749f 100644 --- a/OPENCLAW.md +++ b/OPENCLAW.md @@ -128,5 +128,13 @@ entrypoint. ## Unreleased +- Keep syntax-selected stack positions separate from exception ranges for calls, constructors, property reads, and async continuations. Preserve the positions across bytecode rewriting, optimization, and cache replay. + - Add real byte-based JSC heap allocation sampling with weighted allocating-stack profiles and independent major/minor collection inclusion. - Check allocation sampling across interpreter and JIT tiers before merging engine pull requests. + +## Stack positions + +`CodeBlock::callSitePositionForBytecodeIndex` returns an optional absolute source offset for an exact instruction or async resume label. Stack readers use this position while exception expression ranges and debugger line tables retain their existing divots. RareData owns the two sparse vectors, including decoded cache entries; the bytecode cache revision prevents older layouts from being reused. + +Bun must be rebuilt with its matching stack-reader adapter. It must also preserve built-in error constructor syntax: rewriting `new Error()` as `Error()` changes the position and allows a returning arrow to become a proper tail call. The engine's proper-tail-call behavior is unchanged. The integration continues @robobun’s upstream Bun source-position work in [#35179](https://github.com/oven-sh/bun/pull/35179), [#37396](https://github.com/oven-sh/bun/pull/37396), and [#41580](https://github.com/oven-sh/bun/pull/41580). diff --git a/Source/JavaScriptCore/bytecode/BytecodeOptimizer.cpp b/Source/JavaScriptCore/bytecode/BytecodeOptimizer.cpp index 69ba7bd481d58..b00e8aca93cca 100644 --- a/Source/JavaScriptCore/bytecode/BytecodeOptimizer.cpp +++ b/Source/JavaScriptCore/bytecode/BytecodeOptimizer.cpp @@ -2144,6 +2144,18 @@ void BytecodeOptimizerAccess::emit() return newOffsetForOld(probe); }); + for (auto* positions : { &m_codeBlock->m_callSitePositions, &m_codeBlock->m_asyncResumePositions }) { + positions->removeAllMatching([&](auto& position) { + if (position.first >= m_offsetToIndex.size()) + return true; + unsigned index = m_offsetToIndex[position.first]; + if (index == UINT_MAX || !m_insns[index].live) + return true; + position.first = m_insns[index].newOffset; + return false; + }); + } + // All jumps were sized to fit; nothing is out of line anymore. m_codeBlock->replaceOutOfLineJumpTargets(); diff --git a/Source/JavaScriptCore/bytecode/BytecodeRewriter.h b/Source/JavaScriptCore/bytecode/BytecodeRewriter.h index ab3537881dd50..57feb9a95213b 100644 --- a/Source/JavaScriptCore/bytecode/BytecodeRewriter.h +++ b/Source/JavaScriptCore/bytecode/BytecodeRewriter.h @@ -206,6 +206,17 @@ WTF_MAKE_NONCOPYABLE(BytecodeRewriter); return adjustJumpTarget(InsertionPoint(0, Position::Entrypoint), InsertionPoint(absoluteOffset, Position::LabelPoint)); } + std::optional adjustInstructionOffset(JSInstructionStream::Offset offset) + { + for (const auto& insertion : m_insertions) { + if (insertion.type == Insertion::Type::Remove + && offset >= static_cast(insertion.index.bytecodeOffset) + && offset - insertion.index.bytecodeOffset < insertion.removeLength) + return std::nullopt; + } + return adjustJumpTarget(InsertionPoint(0, Position::Entrypoint), InsertionPoint(offset, Position::OriginalBytecodePoint)); + } + int32_t adjustJumpTarget(JSInstructionStream::Offset originalBytecodeOffset, int32_t originalJumpTarget) { return adjustJumpTarget(InsertionPoint(originalBytecodeOffset, Position::LabelPoint), InsertionPoint(originalJumpTarget, Position::LabelPoint)); diff --git a/Source/JavaScriptCore/bytecode/CodeBlock.cpp b/Source/JavaScriptCore/bytecode/CodeBlock.cpp index ca3f29cb81722..c960c1d248072 100644 --- a/Source/JavaScriptCore/bytecode/CodeBlock.cpp +++ b/Source/JavaScriptCore/bytecode/CodeBlock.cpp @@ -2592,6 +2592,14 @@ ExpressionInfo::Entry CodeBlock::expressionInfoForBytecodeIndex(BytecodeIndex by return entry; } +std::optional CodeBlock::callSitePositionForBytecodeIndex(BytecodeIndex index, bool isAsync) const +{ + auto offset = m_unlinkedCode->callSitePositionForBytecodeIndex(index, isAsync); + if (!offset) + return std::nullopt; + return *offset + sourceOffset(); +} + bool CodeBlock::hasOpDebugForLineAndColumn(unsigned line, std::optional column) { const auto& instructionStream = instructions(); diff --git a/Source/JavaScriptCore/bytecode/CodeBlock.h b/Source/JavaScriptCore/bytecode/CodeBlock.h index 483afcf94e818..25ad03afe38bc 100644 --- a/Source/JavaScriptCore/bytecode/CodeBlock.h +++ b/Source/JavaScriptCore/bytecode/CodeBlock.h @@ -289,6 +289,7 @@ class CodeBlock : public JSCell { // For a thread that runs beside the mutator. It keeps nothing, so it decodes the expression info on every call. LineColumn lineColumnForBytecodeIndexConcurrently(BytecodeIndex) const; ExpressionInfo::Entry expressionInfoForBytecodeIndex(BytecodeIndex) const; + JS_EXPORT_PRIVATE std::optional callSitePositionForBytecodeIndex(BytecodeIndex, bool isAsync = false) const; std::optional bytecodeIndexFromCallSiteIndex(CallSiteIndex); diff --git a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.cpp b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.cpp index d7d0085565b7b..021ffa3a70673 100644 --- a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.cpp +++ b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.cpp @@ -153,6 +153,19 @@ size_t UnlinkedCodeBlock::estimatedSize(JSCell* cell, VM& vm) return Base::estimatedSize(cell, vm) + extraSize; } +std::optional UnlinkedCodeBlock::callSitePositionForBytecodeIndex(BytecodeIndex index, bool isAsync) const +{ + if (!m_rareData) + return std::nullopt; + const auto& positions = isAsync ? m_rareData->m_asyncResumePositions : m_rareData->m_callSitePositions; + auto it = std::lower_bound(positions.begin(), positions.end(), index.offset(), [](const auto& position, unsigned offset) { + return position.first < offset; + }); + if (it == positions.end() || it->first != index.offset()) + return std::nullopt; + return it->second; +} + size_t UnlinkedCodeBlock::RareData::sizeInBytes(const AbstractLocker&) const { size_t size = sizeof(RareData); @@ -162,6 +175,8 @@ size_t UnlinkedCodeBlock::RareData::sizeInBytes(const AbstractLocker&) const size += m_unlinkedStringSwitchJumpTables.byteSize(); size += m_typeProfilerInfoMap.capacity() * sizeof(decltype(m_typeProfilerInfoMap)::KeyValuePairType); size += m_opProfileControlFlowBytecodeOffsets.byteSize(); + size += m_callSitePositions.byteSize(); + size += m_asyncResumePositions.byteSize(); size += m_bitVectors.byteSize(); // FIXME: account for each bit vector. size += m_constantIdentifierSets.byteSize(); diff --git a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h index 4cca7ec3062aa..b841129dbb51d 100644 --- a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h +++ b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h @@ -486,6 +486,9 @@ class UnlinkedCodeBlock : public JSCell { public: using OutOfLineJumpTargets = UncheckedKeyHashMap; + using CallSitePosition = std::pair; + std::optional callSitePositionForBytecodeIndex(BytecodeIndex, bool isAsync) const; + struct RareData { WTF_DEPRECATED_MAKE_STRUCT_FAST_ALLOCATED_WITH_HEAP_IDENTIFIER(RareData, UnlinkedCodeBlock_RareData); @@ -504,6 +507,8 @@ class UnlinkedCodeBlock : public JSCell { }; UncheckedKeyHashMap m_typeProfilerInfoMap; FixedVector m_opProfileControlFlowBytecodeOffsets; + FixedVector m_callSitePositions; + FixedVector m_asyncResumePositions; FixedVector m_bitVectors; FixedVector m_constantIdentifierSets; diff --git a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp index de910491fd379..9ec22d3fea41e 100644 --- a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp +++ b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp @@ -81,6 +81,8 @@ bool UnlinkedCodeBlockGenerator::finalize(std::unique_ptr i || !m_unlinkedStringSwitchJumpTables.isEmpty() || !m_typeProfilerInfoMap.isEmpty() || !m_opProfileControlFlowBytecodeOffsets.isEmpty() + || !m_callSitePositions.isEmpty() + || !m_asyncResumePositions.isEmpty() || !m_bitVectors.isEmpty() || !m_constantIdentifierSets.isEmpty()) m_codeBlock->createRareDataIfNecessary(locker); @@ -92,6 +94,8 @@ bool UnlinkedCodeBlockGenerator::finalize(std::unique_ptr i m_codeBlock->m_rareData->m_unlinkedStringSwitchJumpTables = WTF::move(m_unlinkedStringSwitchJumpTables); m_codeBlock->m_rareData->m_typeProfilerInfoMap = WTF::move(m_typeProfilerInfoMap); m_codeBlock->m_rareData->m_opProfileControlFlowBytecodeOffsets = WTF::move(m_opProfileControlFlowBytecodeOffsets); + m_codeBlock->m_rareData->m_callSitePositions = WTF::move(m_callSitePositions); + m_codeBlock->m_rareData->m_asyncResumePositions = WTF::move(m_asyncResumePositions); m_codeBlock->m_rareData->m_bitVectors = WTF::move(m_bitVectors); m_codeBlock->m_rareData->m_constantIdentifierSets = WTF::move(m_constantIdentifierSets); } @@ -138,6 +142,18 @@ void UnlinkedCodeBlockGenerator::applyModification(BytecodeRewriter& rewriter) m_typeProfilerInfoMap.swap(adjustedTypeProfilerInfoMap); } + m_callSitePositions.removeAllMatching([&](auto& position) { + auto offset = rewriter.adjustInstructionOffset(position.first); + if (!offset) + return true; + position.first = *offset; + return false; + }); + + // Async stack frames use the state switch's resume label, including injected restore instructions. + for (auto& position : m_asyncResumePositions) + position.first = rewriter.adjustAbsoluteOffset(position.first); + Vector bytecodeOffsetAdjustments; rewriter.forEachLabelPoint([&] (int32_t bytecodeOffset) { bytecodeOffsetAdjustments.append(bytecodeOffset); diff --git a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.h b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.h index 13515ff946fe4..a05b48d9c1c15 100644 --- a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.h +++ b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.h @@ -76,6 +76,16 @@ class UnlinkedCodeBlockGenerator { UnlinkedMetadataTable& metadata() { return m_codeBlock->metadata(); } void addExpressionInfo(unsigned instructionOffset, unsigned divot, unsigned startOffset, unsigned endOffset); + void addCallSitePosition(unsigned instructionOffset, unsigned sourceOffset) + { + ASSERT(m_callSitePositions.isEmpty() || m_callSitePositions.last().first < instructionOffset); + m_callSitePositions.append({ instructionOffset, sourceOffset }); + } + void addAsyncResumePosition(unsigned instructionOffset, unsigned sourceOffset) + { + ASSERT(m_asyncResumePositions.isEmpty() || m_asyncResumePositions.last().first < instructionOffset); + m_asyncResumePositions.append({ instructionOffset, sourceOffset }); + } void addTypeProfilerExpressionInfo(unsigned instructionOffset, unsigned startDivot, unsigned endDivot); void addOpProfileControlFlowBytecodeOffset(JSInstructionStream::Offset offset) { @@ -210,6 +220,8 @@ class UnlinkedCodeBlockGenerator { Vector m_unlinkedStringSwitchJumpTables; UncheckedKeyHashMap m_typeProfilerInfoMap; Vector m_opProfileControlFlowBytecodeOffsets; + Vector m_callSitePositions; + Vector m_asyncResumePositions; Vector m_bitVectors; Vector m_constantIdentifierSets; unsigned m_numBinaryArithProfiles { 0 }; diff --git a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp index 6ffe83b8db654..bf1b54116836e 100644 --- a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp +++ b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp @@ -3838,30 +3838,30 @@ RegisterID* BytecodeGenerator::emitAsyncIteratorNext(RegisterID* dst, RegisterID return dst; } -RegisterID* BytecodeGenerator::emitCall(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitCall(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { - return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall); + return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } -void BytecodeGenerator::emitCallIgnoreResult(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +void BytecodeGenerator::emitCallIgnoreResult(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { - emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall); + emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } -RegisterID* BytecodeGenerator::emitCallInTailPosition(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitCallInTailPosition(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { if (m_allowTailCallOptimization) { m_codeBlock->setHasTailCalls(); - return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall); + return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } if (m_allowCallIgnoreResultOptimization) - return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall); - return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall); + return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall, callSitePosition); + return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } -RegisterID* BytecodeGenerator::emitCallDirectEval(RegisterID* dst, RegisterID* func, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitCallDirectEval(RegisterID* dst, RegisterID* func, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { - return emitCall(dst, func, NoExpectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall); + return emitCall(dst, func, NoExpectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } ExpectedFunction BytecodeGenerator::expectedFunctionForIdentifier(const Identifier& identifier) @@ -3939,7 +3939,7 @@ LexicallyScopedFeatures BytecodeGenerator::computeFeaturesForCallDirectEval() } template -RegisterID* BytecodeGenerator::emitCall(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitCall(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { constexpr auto opcodeID = CallOp::opcodeID; ASSERT(opcodeID == op_call || opcodeID == op_call_direct_eval || opcodeID == op_tail_call || opcodeID == op_call_ignore_result); @@ -3962,12 +3962,12 @@ RegisterID* BytecodeGenerator::emitCall(RegisterID* dst, RegisterID* func, Expec emitExpressionInfo(spread->divot(), spread->divotStart(), spread->divotEnd()); OpSpread::emit(this, argumentRegister.get(), argumentRegister.get()); - return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, debuggableCall); + return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } } RefPtr argumentRegister; argumentRegister = expression->emitBytecode(*this, callArguments.argumentRegister(0)); - return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, debuggableCall); + return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } for (; n; n = n->m_next) emitNode(callArguments.argumentRegister(argument++), n); @@ -3989,6 +3989,8 @@ RegisterID* BytecodeGenerator::emitCall(RegisterID* dst, RegisterID* func, Expec if (opcodeID == op_tail_call) emitLogShadowChickenTailIfNecessary(); + emitCallSitePosition(callSitePosition); + // Emit call. ASSERT(dst); ASSERT(dst != ignoredResult()); @@ -4009,21 +4011,21 @@ RegisterID* BytecodeGenerator::emitCall(RegisterID* dst, RegisterID* func, Expec return dst; } -RegisterID* BytecodeGenerator::emitCallVarargs(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitCallVarargs(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { - return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall); + return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } -RegisterID* BytecodeGenerator::emitCallVarargsInTailPosition(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitCallVarargsInTailPosition(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { if (m_allowTailCallOptimization) - return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall); - return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall); + return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall, callSitePosition); + return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } -RegisterID* BytecodeGenerator::emitConstructVarargs(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitConstructVarargs(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { - return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall); + return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } RegisterID* BytecodeGenerator::emitSuperConstructVarargs(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) @@ -4032,7 +4034,7 @@ RegisterID* BytecodeGenerator::emitSuperConstructVarargs(RegisterID* dst, Regist } template -RegisterID* BytecodeGenerator::emitCallVarargs(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitCallVarargs(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { if (shouldEmitDebugHooks() && debuggableCall == DebuggableCall::Yes) emitDebugHook(WillExecuteExpression, divotStart); @@ -4042,6 +4044,8 @@ RegisterID* BytecodeGenerator::emitCallVarargs(RegisterID* dst, RegisterID* func if (VarargsOp::opcodeID == op_tail_call_varargs) emitLogShadowChickenTailIfNecessary(); + emitCallSitePosition(callSitePosition); + // Emit call. ASSERT(dst != ignoredResult()); if constexpr (VarargsOp::opcodeID == op_tail_call_varargs) @@ -4155,7 +4159,7 @@ RegisterID* BytecodeGenerator::emitReturn(RegisterID* src) } template -RegisterID* BytecodeGenerator::emitConstructImpl(RegisterID* dst, RegisterID* func, RegisterID* lazyThis, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, bool isDefaultDerivedConstructorCall) +RegisterID* BytecodeGenerator::emitConstructImpl(RegisterID* dst, RegisterID* func, RegisterID* lazyThis, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, bool isDefaultDerivedConstructorCall, std::optional callSitePosition) { ASSERT(func->refCount()); @@ -4180,13 +4184,13 @@ RegisterID* BytecodeGenerator::emitConstructImpl(RegisterID* dst, RegisterID* fu } move(callArguments.thisRegister(), lazyThis); - return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, DebuggableCall::No); + return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, DebuggableCall::No, callSitePosition); } } RefPtr argumentRegister; argumentRegister = expression->emitBytecode(*this, callArguments.argumentRegister(0)); move(callArguments.thisRegister(), lazyThis); - return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, DebuggableCall::No); + return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, DebuggableCall::No, callSitePosition); } for (ArgumentListNode* n = argumentsNode->m_listNode; n; n = n->m_next) @@ -4205,6 +4209,7 @@ RegisterID* BytecodeGenerator::emitConstructImpl(RegisterID* dst, RegisterID* fu Ref