Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8231,9 +8231,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}

/// Undo every parse-pass mutation made since [`Self::parser_snapshot`].
pub(crate) fn restore_parser_snapshot(&mut self, snapshot: ParserSnapshot<'a>) {
pub(crate) fn restore_parser_snapshot(&mut self, snapshot: &mut ParserSnapshot<'a>) {
self.lexer.restore(&snapshot.lexer);
self.lexer.comments_to_preserve_before = snapshot.comments_to_preserve_before;
self.lexer.comments_to_preserve_before =
core::mem::take(&mut snapshot.comments_to_preserve_before);

let log = self.log();
log.msgs.truncate(snapshot.log_msgs_len);
Expand All @@ -8247,7 +8248,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
snapshot.has_non_local_export_declare_inside_namespace;
self.should_fold_typescript_constant_expressions =
snapshot.should_fold_typescript_constant_expressions;
self.fn_or_arrow_data_parse = snapshot.fn_or_arrow_data_parse;
self.fn_or_arrow_data_parse = snapshot.fn_or_arrow_data_parse.clone();
self.latest_arrow_arg_loc = snapshot.latest_arrow_arg_loc;
self.forbid_suffix_after_as_loc = snapshot.forbid_suffix_after_as_loc;
self.after_arrow_body_loc = snapshot.after_arrow_body_loc;
Expand Down
16 changes: 15 additions & 1 deletion src/js_parser/parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,10 +575,17 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if Self::IS_TYPESCRIPT_ENABLED && p.lexer.token == T::TColon && invalid_log.is_empty() {
if opts.is_after_question_and_before_colon {
// Only do this very expensive check if we must
is_arrow_fn = p
let parsed;
(is_arrow_fn, parsed) = p
.is_type_script_arrow_return_type_after_question_and_before_colon(
&arrow_data,
)?;
if let Some(arrow) = parsed {
// Parsed without its arguments, for an outer attempt that restores
debug_assert!(p.lexer.is_log_disabled);
p.pop_scope();
return Ok(p.new_expr(arrow, loc));
}
if is_arrow_fn {
// We know this will succeed because we've already done it once above
p.lexer.next()?;
Expand Down Expand Up @@ -1613,6 +1620,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
result
}

/// Whether the lexer can read the token after the current one. Not inlined,
/// so that the lexer snapshot stays out of the frame of a caller that recurses.
#[inline(never)]
pub(crate) fn can_read_next_token(&mut self) -> bool {
self.next_token_matches(|_| true)
}

#[inline]
fn check_for_arrow_after_the_current_token(&mut self) -> bool {
self.next_token_matches(|p| p.lexer.token == T::TEqualsGreaterThan)
Expand Down
33 changes: 26 additions & 7 deletions src/js_parser/parse/parse_skip_typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::parser::{
use crate::typescript;
use crate::typescript::SkipTypeOptions;
use crate::typescript::identifier::{Kind as TsIdentKind, kind_for_identifier};
use bun_ast::E;
use bun_ast::op::Level;
use bun_ast::ts::Metadata;

Expand Down Expand Up @@ -1598,37 +1599,55 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
/// that its members stay untouched. The outcome is memoized by the offset
/// of the ":" in `ts_conditional_arrow_attempts`, so the real parse does not
/// repeat the attempts nested inside the body.
///
/// An attempt that runs inside another attempt is thrown away with it. When
/// it finds an arrow function, it restores nothing and returns the arrow
/// function, and the caller uses that in place of a second parse. Without
/// this, every attempt around a body parses that body one more time. The
/// log is off on entry only inside another attempt: nothing else that turns
/// it off parses an expression.
// Not inlined: `parse_paren_expr` recurses and the snapshot is large.
#[cold]
#[inline(never)]
pub(crate) fn is_type_script_arrow_return_type_after_question_and_before_colon(
&mut self,
arrow_data: &FnOrArrowDataParse,
) -> Result<bool, Error> {
) -> Result<(bool, Option<E::Arrow>), Error> {
self.mark_type_script_only();
debug_assert!(self.lexer.start <= (u32::MAX >> 1) as usize);
let memo_key = self.lexer.start as u32;
if let Ok(i) = self
.ts_conditional_arrow_attempts
.binary_search_by_key(&memo_key, |&packed| packed >> 1)
{
return Ok(self.ts_conditional_arrow_attempts[i] & 1 == 1);
return Ok((self.ts_conditional_arrow_attempts[i] & 1 == 1, None));
}

let snapshot = self.parser_snapshot();
let mut snapshot = self.parser_snapshot();
let nested = self.lexer.is_log_disabled;
self.lexer.is_log_disabled = true;
// Metadata looks up type names, which adds unbound symbols to the module scope.
let emit_decorator_metadata =
core::mem::take(&mut self.options.features.emit_decorator_metadata);

let mut data = arrow_data.clone();
let mut parsed = None;
let result: Result<(), Error> = (|| {
self.lexer.expect(T::TColon)?;
self.skip_typescript_return_type()?;
self.parse_arrow_body(&mut [], &mut data)?;
// The ":" that pairs with the "?"
let arrow = self.parse_arrow_body(&mut [], &mut data)?;
// The ":" that pairs with the "?". As `expect` does, read the token after it.
if nested && self.lexer.token == T::TColon && self.can_read_next_token() {
parsed = Some(arrow);
return Ok(());
}
self.lexer.expect(T::TColon)?;
Ok(())
})();

self.restore_parser_snapshot(snapshot);
if parsed.is_none() {
self.restore_parser_snapshot(&mut snapshot);
}
Comment thread
robobun marked this conversation as resolved.
Resolved
self.options.features.emit_decorator_metadata = emit_decorator_metadata;
let is_arrow_fn = match result {
Ok(()) => true,
Expand All @@ -1646,7 +1665,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
self.ts_conditional_arrow_attempts
.insert(insert_at, (memo_key << 1) | is_arrow_fn as u32);
}
Ok(is_arrow_fn)
Ok((is_arrow_fn, parsed))
}

// ─────────────────────── try_* wrappers ───────────────────────
Expand Down
81 changes: 80 additions & 1 deletion test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { describe, expect, it } from "bun:test";
import { bunEnv, bunExe, bunRun, hideFromStackTrace, integersThatNeedRounding, isDebug, tempDir } from "harness";
import {
bunEnv,
bunExe,
bunRun,
hideFromStackTrace,
integersThatNeedRounding,
isASAN,
isDebug,
runFixtureMaxRSS,
tempDir,
} from "harness";
import { join } from "path";

describe("Bun.Transpiler", () => {
Expand Down Expand Up @@ -463,20 +473,89 @@ describe("Bun.Transpiler", () => {
exp("x = a ? /*! L */ (b) : c => { return d }", "x = a ? b : (c) => {\n /*! L */\n return d;\n};\n");
exp('x = a ? /*! L */ (b) : T => import("m") : c', 'x = a ? (b) => import("m") : c;\n');

// An attempt in the body of another attempt keeps the arrow function that
// it finds, and the outer attempt throws it away. The parse after the
// outer attempt declares the parameters, reports the errors and gets the
// legal comments.
exp("x = a ? (b) : c => [a ? (b) : T => d : e]", "x = a ? b : (c) => [a ? (b) => d : e];\n");
exp("x = a ? (b) : T => [a ? (b) : c => d] : e", "x = a ? (b) => [a ? b : (c) => d] : e;\n");
exp(
"x = a ? async (b) : c => a ? async (b) : c => await d : e : e",
"x = a ? async (b) => a ? async (b) => await d : e : e;\n",
);
exp(
"x = a ? (b) : T => a ? (c, ...d) : T => [b, c, d] : e : e",
"x = a ? (b) => a ? (c, ...d) => [b, c, d] : e : e;\n",
);
err(
"x = a ? (b) : T => a ? (c, c) : T => 1 : e : e",
'"c" cannot be bound multiple times in the same parameter list',
);
err("x = a ? (b) : T => a ? (b) : T => { let b } : e : e", '"b" has already been declared');
exp(
"x = a ? /*! L1 */ (b) : T => a ? /*! L2 */ (b) : T => d : e : e; y",
"x = a ? (b) => a ? (b) => d : e : e;\n/*! L1 */\n/*! L2 */\ny;\n",
);
exp(
"x = a ? /*! L1 */ (b) : c => { return a ? /*! L2 */ (b) : T => { let y = 1 } : c }",
"x = a ? b : (c) => {\n /*! L1 */\n return a ? (b) => {\n /*! L2 */\n let y = 1;\n } : c;\n};\n",
);
// As an outermost attempt does, the inner attempt finds no arrow function
// when the lexer cannot read the token after the second ":".
err("x = a ? (b) : T => a ? (b) : T => d : 'abc : e", 'Expected ";" but found ":"');
// A failed attempt leaves no declaration of its body behind.
exp("x = a ? (b) : c => { let b }", "x = a ? b : (c) => {\n let b;\n};\n");
exp("x = a ? (b) : c => { class b {} }", "x = a ? b : (c) => {\n class b {\n }\n};\n");
// An attempt that ended does not make the next attempt an inner one.
exp(
"x = a ? (b) : c => d; y = a ? (b) : T => b : e; z = a ? (b) : T => b : e",
"x = a ? b : (c) => d;\ny = a ? (b) => b : e;\nz = a ? (b) => b : e;\n",
);

// The attempt parses the body, so an attempt nested in the body must not
// run again when the body is parsed for real: 2^40 parses would hang.
let kept = "d";
let discarded = "d";
let keptInDiscarded = "d";
let keptOut = "d";
let discardedOut = "d";
let keptInDiscardedOut = "d";
for (let i = 0; i < 40; i++) {
kept = `a ? (b) : c => ${kept} : e`;
keptOut = `a ? (b) => ${keptOut} : e`;
discarded = `a ? (b) : c => ${discarded}`;
discardedOut = `a ? b : (c) => ${discardedOut}`;
keptInDiscarded = i < 20 ? `a ? (b) : c => ${keptInDiscarded} : e` : `a ? (b) : c => ${keptInDiscarded}`;
keptInDiscardedOut = i < 20 ? `a ? (b) => ${keptInDiscardedOut} : e` : `a ? b : (c) => ${keptInDiscardedOut}`;
}
exp(`x = ${kept}`, `x = ${keptOut};\n`);
exp(`x = ${discarded}`, `x = ${discardedOut};\n`);
exp(`x = ${keptInDiscarded}`, `x = ${keptInDiscardedOut};\n`);
});

it("nested arrow functions with return types between ? and : take linear memory", async () => {
// Each arrow function here has the next one in its body, and every body
// was parsed again for each arrow function around it. Depth n took
// n * n / 2 parses of a level, and what a parse allocates stays until
// the file is done.
const depth = isDebug || isASAN ? 256 : 640;
const fixture = returnType => /* js */ `
let input = "d";
let output = "d";
for (let i = 0; i < ${depth}; i++) {
input = "a ? (b)${returnType} => " + input + " : e";
output = "a ? (b) => " + output + " : e";
}
const printed = new Bun.Transpiler({ loader: "ts" }).transformSync("x = " + input);
console.log(JSON.stringify({ printed: printed === "x = " + output + ";\\n" }));
`;
const [withReturnTypes, without] = await Promise.all([
runFixtureMaxRSS(fixture(" : c"), { printed: true }),
runFixtureMaxRSS(fixture(""), { printed: true }),
]);
// Peak RSS over the same input without return types. Before: 225 MB in
// release, 38 MB in a debug ASAN build. After: under 1 MB in both.
expect((withReturnTypes - without) / 1024 / 1024).toBeLessThan(isASAN || isDebug ? 10 : 32);
});

it("emitDecoratorMetadata in an arrow function with a return type in a true branch", async () => {
Expand Down
Loading