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
22 changes: 5 additions & 17 deletions src/aot/lower/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ use core::cell::{OnceCell, RefCell};
use core::mem::take;
use std::borrow::Cow;

use bun_core::printer::{FIRST_ASCII, LAST_ASCII, bmp_escape, surrogate_pair_escape};
use bun_core::strings::{CodepointIterator, Cursor};
use bun_core::printer::{RawText, write_raw_text_ascii};
use bun_sema::atom::Atom;
use bun_sema::bind::{Binding, Bound, SymbolId};
use bun_sema::check::aot::{BuiltinTypeSymbols, NamedValues, Queries, QueryInputs};
Expand Down Expand Up @@ -56,23 +55,12 @@ use crate::statistics::{LimitId, LimitValues};
pub(crate) use self::function::FunctionLowering;

pub(super) fn regexp_pattern_as_printed(written: &[u8]) -> Cow<'_, [u8]> {
let is_printed_as_written = |character: u32| (FIRST_ASCII..=LAST_ASCII).contains(&character);
if written
.iter()
.all(|&byte| is_printed_as_written(u32::from(byte)))
{
let Some(start) = RawText::RegExp.first_respelled(written) else {
return Cow::Borrowed(written);
}
};
let mut printed = Vec::with_capacity(written.len() * 2);
let characters = CodepointIterator::init(written);
let mut cursor = Cursor::default();
while characters.next(&mut cursor) {
match cursor.c as u32 {
character if is_printed_as_written(character) => printed.push(character as u8),
character @ 0..=0xFFFF => printed.extend_from_slice(&bmp_escape(character)),
character => printed.extend_from_slice(&surrogate_pair_escape(character)),
}
}
write_raw_text_ascii(written, start, RawText::RegExp, &mut printed)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
.expect("a Vec takes every write");
Cow::Owned(printed)
}

Expand Down
20 changes: 4 additions & 16 deletions src/aot/lower/quoted_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use bun_sort_by_number::SortByNumber;
use rustc_hash::FxHashMap;
use smallvec::SmallVec;

use super::context::regexp_pattern_as_printed;
use super::expression::is_link_in_brackets;
use super::{FileLowering, FunctionLowering};
use crate::ir::generated::QuotedText;
Expand Down Expand Up @@ -787,22 +788,9 @@ impl Printer<'_, '_, '_, '_, '_> {
ExprKind::Regex { .. } => {
self.space_before_name();
let start = whole(self);
let characters = CodepointIterator::init(
&hir.text[hir[expression].pos as usize..hir[expression].end as usize],
);
let mut cursor = Cursor::default();
while characters.next(&mut cursor) {
match (u8::try_from(cursor.c), char::from_u32(cursor.c as u32)) {
(Ok(character), _) if character.is_ascii() => self.text.push(character),
(_, Some(character)) => {
for unit in character.encode_utf16(&mut [0; 2]) {
self.text
.extend_from_slice(format!("\\u{unit:04X}").as_bytes());
}
}
(_, None) => self.gaps += 1,
}
}
let literal = &hir.text[hir[expression].pos as usize..hir[expression].end as usize];
self.text
.extend_from_slice(&regexp_pattern_as_printed(literal));
start
}
ExprKind::Spread(operand) => {
Expand Down
114 changes: 113 additions & 1 deletion src/bun_core/string/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2057,7 +2057,7 @@ pub mod printer {
use crate::string::immutable::{self as strings, Encoding as StrEncoding};
use crate::string::mutable_string::MutableString;

use crate::fmt::{hex_byte_lower, hex2_upper, hex4_upper};
use crate::fmt::{hex_byte_lower, hex2_upper, hex4_upper, u64_hex_var_lower};

pub const FIRST_ASCII: u32 = 0x20;
pub const LAST_ASCII: u32 = 0x7E;
Expand Down Expand Up @@ -2089,6 +2089,118 @@ pub mod printer {
/// `MutableString`, and any other `crate::io::Write` sink.
pub use crate::io::Write as PrinterWriter;

/// Source text that the printer holds as it is written, backslashes included.
#[derive(Clone, Copy)]
pub enum RawText {
/// One part of a tagged template.
Template,
/// A regular expression literal, or its pattern alone.
RegExp,
}

impl RawText {
#[inline]
fn is_respelled(self, character: u32) -> bool {
match self {
RawText::Template => character >= 0x80,
RawText::RegExp => !(FIRST_ASCII..=LAST_ASCII).contains(&character),
}
}

/// Where `write_raw_text_ascii` first writes an escape. `None`: the text is printed as it is.
#[inline]
pub fn first_respelled(self, text: &[u8]) -> Option<usize> {
match self {
RawText::Template => strings::first_non_ascii_usize(text),
RawText::RegExp => text
.iter()
.position(|&byte| self.is_respelled(u32::from(byte))),
}
}
}

/// Writes `text` in ASCII: an escape stands for each code point that is not printed as it is.
#[inline(always)] // Each caller names its kind, so the loop is compiled for that kind alone.
pub fn write_raw_text_ascii<W: PrinterWriter + ?Sized>(
text: &[u8],
start: usize,
kind: RawText,
writer: &mut W,
) -> crate::CrateResult<()> {
debug_assert_eq!(kind.first_respelled(text), Some(start));
let characters = strings::CodepointIterator::init(text);
let mut cursor = strings::Cursor {
i: start as u32,
..Default::default()
};
let mut copied = 0;
while characters.next(&mut cursor) {
let character = cursor.c as u32;
if !kind.is_respelled(character) {
continue;
}
let at = cursor.i as usize;
if copied < at {
writer.write_all(&text[copied..at])?;
}
// A backslash is printed as it is, so only what was copied just now can end in one.
let is_behind_backslash = copied < at && text[at - 1] == b'\\';
copied = at + usize::from(cursor.width);
if is_behind_backslash && write_behind_backslash(&text[..at], character, kind, writer)?
{
continue;
}
match (kind, character) {
(_, 0..=0xFFFF) => writer.write_all(&bmp_escape(character))?,
(RawText::Template, _) => write_braced_escape(b"\\u{", character, writer)?,
(RawText::RegExp, _) => writer.write_all(&surrogate_pair_escape(character))?,
}
}
if copied < text.len() {
writer.write_all(&text[copied..])?;
}
Ok(())
}

/// Where the last backslash of `ahead` escapes `character`, writes the escape without its own backslash.
#[cold]
#[inline(never)]
fn write_behind_backslash<W: PrinterWriter + ?Sized>(
ahead: &[u8],
character: u32,
kind: RawText,
writer: &mut W,
) -> crate::CrateResult<bool> {
let mut run = 0;
while run < ahead.len() && ahead[ahead.len() - 1 - run] == b'\\' {
run += 1;
}
if run % 2 == 0 {
return Ok(false);
}
match (kind, character) {
// A line continuation. No regular expression literal holds a line terminator.
(RawText::Template, 0x2028 | 0x2029) => writer.write_all(b"\n")?,
(_, 0..=0xFFFF) => writer.write_all(&bmp_escape(character)[1..])?,
Comment thread
robobun marked this conversation as resolved.
Resolved
Comment thread
robobun marked this conversation as resolved.
Resolved
(RawText::Template, _) => write_braced_escape(b"u{", character, writer)?,
(RawText::RegExp, _) => writer.write_all(&surrogate_pair_escape(character)[1..])?,
}
Ok(true)
}

/// `\u{1f600}`, where `opening` is `\u{` or `u{`.
#[inline]
fn write_braced_escape<W: PrinterWriter + ?Sized>(
opening: &[u8],
character: u32,
writer: &mut W,
) -> crate::CrateResult<()> {
let mut digits = [0; 16];
writer.write_all(opening)?;
writer.write_all(u64_hex_var_lower(&mut digits, u64::from(character)))?;
writer.write_all(b"}")
}

#[inline]
fn can_print_without_escape(c: i32, ascii_only: bool) -> bool {
if c <= LAST_ASCII as i32 {
Expand Down
9 changes: 6 additions & 3 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use bun_collections::VecExt;
use std::io::Write as _;

use bstr::BStr;
use bun_core::printer::RawText;
use bun_core::strings;

use crate::lexer as js_lexer;
Expand Down Expand Up @@ -816,14 +817,16 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
return None;
};
let head = *head;
let mut is_ascii = strings::is_all_ascii(head.slice());
let is_printed_as_written = |text: &[u8]| RawText::Template.first_respelled(text).is_none();
let mut is_all_printed_as_written = is_printed_as_written(head.slice());
for part in template.parts() {
let E::TemplateContents::Raw(tail) = &part.tail else {
return None;
};
is_ascii = is_ascii && strings::is_all_ascii(tail.slice());
is_all_printed_as_written =
is_all_printed_as_written && is_printed_as_written(tail.slice());
}
if is_ascii {
if is_all_printed_as_written {
return None;
}

Expand Down
91 changes: 14 additions & 77 deletions src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -861,8 +861,8 @@ pub type RuntimeTranspilerCacheRef = core::ptr::NonNull<bun_ast::RuntimeTranspil

use bun_core::fmt::hex2_upper; // remaining `\xHH` site below
use bun_core::printer::{
FIRST_ASCII, FIRST_HIGH_SURROGATE, LAST_ASCII, LAST_LOW_SURROGATE, bmp_escape,
surrogate_pair_escape,
FIRST_ASCII, FIRST_HIGH_SURROGATE, LAST_ASCII, LAST_LOW_SURROGATE, RawText, bmp_escape,
surrogate_pair_escape, write_raw_text_ascii,
};

/// For support JavaScriptCore
Expand Down Expand Up @@ -3164,53 +3164,20 @@ pub(crate) mod __gated_printer {
}

fn print_raw_template_literal(&mut self, bytes: &[u8]) {
if IS_JSON || !ASCII_ONLY {
if !IS_JSON && ASCII_ONLY {
self.print_raw_text_ascii(bytes, RawText::Template);
} else {
self.print(bytes);
return;
}

// Translate any non-ASCII to unicode escape sequences
// Note that this does not correctly handle malformed template literal strings
// template literal strings can contain invalid unicode code points
// and pretty much anything else
//
// we use WTF-8 here, but that's still not good enough.
//
let mut ascii_start: usize = 0;
let mut is_ascii = false;
let iter = CodepointIterator::init(bytes);
let mut cursor = strings::Cursor::default();

while iter.next(&mut cursor) {
match cursor.c as u32 {
// unlike other versions, we only want to mutate > 0x7F
0..=LAST_ASCII => {
if !is_ascii {
ascii_start = cursor.i as usize;
is_ascii = true;
}
}
_ => {
if is_ascii {
self.print(&bytes[ascii_start..(cursor.i as usize)]);
is_ascii = false;
}

match cursor.c as u32 {
c @ 0..=0xFFFF => self.print(&bmp_escape(c)[..]),
_ => {
self.print(b"\\u{");
let _ = self.fmt(format_args!("{:x}", cursor.c));
self.print(b"}");
}
}
}
}
}
}

if is_ascii {
self.print(&bytes[ascii_start..]);
}
#[inline(always)]
fn print_raw_text_ascii(&mut self, text: &[u8], kind: RawText) {
let Some(start) = kind.first_respelled(text) else {
return self.print(text);
};
let mut writer = self.writer.std_writer();
let _ = write_raw_text_ascii(text, start, kind, &mut writer);
}

pub(crate) fn check_stack_overflow(&self) -> crate::Result<()> {
Expand Down Expand Up @@ -4678,38 +4645,8 @@ pub(crate) mod __gated_printer {
}

if IS_BUN_PLATFORM {
// Translate any non-ASCII to unicode escape sequences
let mut ascii_start: usize = 0;
let mut is_ascii = false;
let iter = CodepointIterator::init(&e.value);
let mut cursor = strings::Cursor::default();
while iter.next(&mut cursor) {
match cursor.c as u32 {
FIRST_ASCII..=LAST_ASCII => {
if !is_ascii {
ascii_start = cursor.i as usize;
is_ascii = true;
}
}
_ => {
if is_ascii {
self.print(&e.value[ascii_start..(cursor.i as usize)]);
is_ascii = false;
}

match cursor.c as u32 {
c @ 0..=0xFFFF => self.print(&bmp_escape(c)[..]),
c => self.print(&surrogate_pair_escape(c)[..]),
}
}
}
}

if is_ascii {
self.print(&e.value[ascii_start..]);
}
self.print_raw_text_ascii(&e.value, RawText::RegExp);
Comment thread
robobun marked this conversation as resolved.
Resolved
} else {
// UTF8 sequence is fine
self.print(&e.value[..]);
}

Expand Down
3 changes: 2 additions & 1 deletion src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ bun_core::declare_scope!(cache, visible);
/// Version 50: The body of a class with lowered class decorators names the class by the
/// binding that holds what the decorators return.
/// Version 51: A `bun:` specifier that names no builtin is printed as it is written.
const EXPECTED_VERSION: u32 = 51;
/// Version 52: A backslash ahead of a character that is printed as an escape starts that escape (#26785).
const EXPECTED_VERSION: u32 = 52;

/// 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
Expand Down
13 changes: 13 additions & 0 deletions test/aot/ir/lower/expressions/regexp-literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,20 @@
// CHECK-NOT: NewRegExp
// CHECK-LABEL: function {{*}}name="create"
// CHECK: = NewRegExp pattern="a+" regExpFlags=[Global, IgnoreCase]
// CHECK-LABEL: function {{*}}name="escaped"
// CHECK: = NewRegExp pattern="\\u00E9+"
// CHECK: = NewRegExp pattern="[\\u00E9-\\u00FC]"
// CHECK: = NewRegExp pattern="\\\\\\u00E9"
// CHECK: = NewRegExp pattern="\\\\\\u00E9"
// CHECK: = NewRegExp pattern="\\uD83D\\uDE00"
// CHECK: = NewRegExp pattern="(?<a>\\u00E9)"

export function create() {
return /a+/gi;
}

// The pattern is what `bun run` prints for the literal: an escape for each non-ASCII character, and a backslash
// ahead of the character is the backslash of that escape.
export function escaped() {
return [/\é+/, /[\é-\ü]/, /\\é/, /\\\é/, /\😀/, /(?<a>\é)/];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// A message quotes a regular expression as Bun prints it: a backslash ahead of a character that is printed as an escape starts that escape, and a control character (the tab of the last but one line) has its escape.
const say = run => { try { run(); } catch (error) { console.log(JSON.stringify(error.message)); } };
const o = {};
say(() => o.nothing(/\é/));
say(() => o.nothing(/\\é/, /\\\é/, /[a-\ÿ]/));
say(() => o.nothing(/\日+/, /\😀/, /[\😀]/u));
say(() => o.nothing(/a b/, /a\ b/));
say(() => /\é/.nothing());
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// A backslash ahead of a non-ASCII character of a literal: alone, in a class, in a range, behind other backslashes, and beside the name of a group. A tag gets the character in its string.
const subjects = ["é", "ü", "\\é", "\\u00E9", "u00E9", "日日", "😀", "\ud83d", "\ude00", "aé", "aaé", "xé", "u{é}", "k<a é>", "a"];
const expressions = [
/\é/, /[\é]/, /[^\é]/, /[\é-\ü]/, /\\é/, /\\\é/, /\é\é/, /^\日+$/, /^\😀$/, /[\😀]/, /^\😀+$/,
/(?<é>a)\é/, /(?<n>a)\k<n>\é/, /(?<=\é)a|\é/, /\x\é/, /\u{\é}/, /\k<a \é>/,
];
for (const expression of expressions)
console.log(expression.source, subjects.map(subject => JSON.stringify(subject.match(expression))).join(" "));
const tag = strings => JSON.stringify([...strings]);
console.log(tag`\é`, tag`a\日b${1}\😀`, tag`\\é\\\é`, tag`\u{zz}\é`);
Loading
Loading