Skip to content
Merged
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
2 changes: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "1817c3c37f5dfc004a33dafac9f6adcf1a9641a4";
export const WEBKIT_VERSION = "c4ddc0cf5255ec9cc209e6369292739b78e3ca80";

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
76 changes: 59 additions & 17 deletions src/bun_core/string/immutable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2624,36 +2624,71 @@ pub fn to_utf16_alloc(

/// WTF-8 → UTF-16LE iff `bytes` contains any non-ASCII byte; pure-ASCII inputs return `None`.
pub fn wtf8_to_utf16_alloc(bytes: &[u8]) -> Option<Vec<u16>> {
first_non_ascii(bytes)?;
let first_non_ascii = first_non_ascii_usize(bytes)?;
let mut out: Vec<u16> = Vec::with_capacity(bytes.len());
// SAFETY: `bytes.len()` u16 of capacity is the bound `write_wtf8_as_utf16le` requires.
unsafe {
let n = write_wtf8_as_utf16le(bytes, first_non_ascii, out.as_mut_ptr().cast::<u8>());
out.set_len(n / 2);
}
Some(out)
}

let out_length = simdutf::length::utf16::from::utf8(bytes);
let mut out: Vec<u16> = Vec::with_capacity(out_length.max(1));
// SAFETY: `out` has ≥ `out_length` u16 of capacity; simdutf writes at most that many.
/// Writes `bytes` (WTF-8) as little-endian UTF-16 code units starting at `dst` and returns the
/// number of bytes written. `first_non_ascii` is the caller's `strings::first_non_ascii(bytes)`:
/// that prefix is widened directly and only the rest goes through simdutf; a lone surrogate or an
/// invalid byte there falls to a scalar loop (invalid byte → U+FFFD, lone surrogate kept).
///
/// # Safety
/// `dst` must be 2-byte aligned and valid for `2 * bytes.len()` bytes of writes (every input byte yields at
/// most one unit); `first_non_ascii <= bytes.len()` and `bytes[..first_non_ascii]` is ASCII.
pub unsafe fn write_wtf8_as_utf16le(bytes: &[u8], first_non_ascii: usize, dst: *mut u8) -> usize {
debug_assert!(dst.addr().is_multiple_of(2) && is_all_ascii(&bytes[..first_non_ascii]));
for (i, &b) in bytes[..first_non_ascii].iter().enumerate() {
// SAFETY: `2 * i + 1 < 2 * bytes.len()`.
unsafe {
dst.add(2 * i)
.cast::<[u8; 2]>()
.write_unaligned(u16::from(b).to_le_bytes())
};
}
let mut written = 2 * first_non_ascii;
let bytes = &bytes[first_non_ascii..];
#[expect(
clippy::cast_ptr_alignment,
reason = "caller contract: dst is 2-byte aligned"
)]
// SAFETY: caller contract; simdutf writes at most `utf16_length_from_utf8(bytes) <= bytes.len()` units.
let res = unsafe {
simdutf::simdutf__convert_utf8_to_utf16le_with_errors(
bytes.as_ptr(),
bytes.len(),
out.as_mut_ptr(),
dst.add(written).cast::<u16>(),
)
};
if res.is_successful() && out_length > 0 {
// SAFETY: on success simdutf initialised exactly `out_length` u16s.
unsafe { out.set_len(out_length) };
return Some(out);
}

out.reserve(bytes.len());
if res.is_successful() {
return written + res.count * 2;
}
let mut put = |unit: u16| {
// SAFETY: at most one unit per input byte consumed, within the caller's `2 * bytes.len()`.
unsafe {
dst.add(written)
.cast::<[u8; 2]>()
.write_unaligned(unit.to_le_bytes())
};
written += 2;
};
let mut i = 0usize;
while i < bytes.len() {
let b = bytes[i];
if b < 0x80 {
out.push(u16::from(b));
put(u16::from(b));
i += 1;
continue;
}
let width = wtf8_byte_sequence_length_with_invalid(b);
if width == 1 {
out.push(UNICODE_REPLACEMENT as u16);
put(UNICODE_REPLACEMENT as u16);
i += 1;
continue;
}
Expand All @@ -2662,14 +2697,21 @@ pub fn wtf8_to_utf16_alloc(bytes: &[u8]) -> Option<Vec<u16>> {
buf[..take].copy_from_slice(&bytes[i..i + take]);
let cp = decode_wtf8_rune_t::<i32>(buf, width, -1);
if cp < 0 {
out.push(UNICODE_REPLACEMENT as u16);
put(UNICODE_REPLACEMENT as u16);
i += 1;
continue;
}
push_codepoint_utf16(&mut out, cp as u32);
let cp = cp as u32;
if cp < 0x10000 {
put(cp as u16);
} else {
let c = cp - 0x10000;
put(0xD800 + (c >> 10) as u16);
put(0xDC00 + (c & 0x3FF) as u16);
}
i += take;
}
Some(out)
written
}

/// `PATTERN_KEY_COMPARE` from the Node.js ESM resolution spec — the comparator
Expand Down
11 changes: 11 additions & 0 deletions src/bun_core/string/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ unsafe extern "C" {
len: usize,
hash: u32,
) -> String;
fn BunString__createStaticExternalUTF16WithHash(
units: *const u16,
len: usize,
hash: u32,
) -> String;
fn BunString__createExternal(
bytes: *const u8,
len: usize,
Expand Down Expand Up @@ -377,6 +382,12 @@ impl String {
// ptr/len without copying or freeing.
unsafe { BunString__createStaticExternal(units.as_ptr().cast::<u8>(), units.len(), false) }
}
/// [`Self::create_static_external_utf16`] for units whose `WTF::StringImpl::hash()` is already known.
pub fn create_static_external_utf16_with_hash(units: &[u16], hash: u32) -> Self {
debug_assert!(!units.is_empty());
// SAFETY: as above; `hash` is StringImpl::hash() of `units`.
unsafe { BunString__createStaticExternalUTF16WithHash(units.as_ptr(), units.len(), hash) }
}
/// Formats `args` into a WTF-backed string; an argument-free ASCII
/// literal is returned as `static_` without copying.
pub fn create_format(args: core::fmt::Arguments<'_>) -> Self {
Expand Down
178 changes: 148 additions & 30 deletions src/bundler/analyze_transpiled_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,47 @@ pub enum ModuleInfoError {
/// in place; nothing is widened or copied.
///
/// `body` / `table` are lifetime-erased: they point either into the
/// executable's mapped section (`'static`) or into `owned`, which lives as long
/// as `self`.
/// executable's mapped section (`'static`) or into the `Owned` variant's bytes,
/// which live as long as `self`.
pub struct ModuleInfoDeserialized {
body: Body<'static>,
table: ModuleInfoStringTable<'static>,
table: ModuleInfoStrings<'static>,
pub flags: Flags,
/// Backing storage for `body`/`table`: `None` when they live in the
/// executable's mapped section — the ids then index the shared string
/// table and identifiers come from the VM-wide slots
/// (`Bun__VM__sharedModuleInfoIdentifiers`) rather than a per-record array.
owned: Option<Box<[u8]>>,
}

/// Where a record's names come from: a self-contained record carries its own
/// strings (`WTF::StringImpl` bodies the runtime atomizes) and the bytes they
/// live in; a record inside an executable indexes the slot table every module
/// of the executable shares, resolved by JSC as the bytecode's own string slots are.
pub enum ModuleInfoStrings<'a> {
Owned {
table: ModuleInfoStringTable<'a>,
_bytes: Box<[u8]>,
},
Shared(ModuleInfoSlotTable<'a>),
}
/// One name of a record, as the runtime resolves it.
#[derive(Clone, Copy)]
pub enum ModuleInfoString<'a> {
/// `is_8bit` Latin-1 bytes, else 2-byte-aligned little-endian UTF-16.
Chars { chars: &'a [u8], is_8bit: bool },
/// `EncoderStringTable::slotFor`.
Slot(u32),
}
impl<'a> ModuleInfoStrings<'a> {
pub fn count(&self) -> u32 {
match self {
Self::Owned { table, .. } => table.count,
Self::Shared(t) => t.count(),
}
}
/// String `id`, or `None` if out of bounds (corrupt input).
pub fn get(&self, id: u32) -> Option<ModuleInfoString<'a>> {
match self {
Self::Owned { table, .. } => table.get(id),
Self::Shared(t) => t.get(id).map(ModuleInfoString::Slot),
}
}
}

/// The body split into its regions; every count has been bounds-checked
Expand Down Expand Up @@ -222,31 +252,26 @@ impl ModuleInfoDeserialized {
&self.body
}
#[inline]
pub fn table(&self) -> &ModuleInfoStringTable<'_> {
&self.table
}
#[inline]
pub fn ids(&self) -> IdCursor<'_> {
IdCursor {
rem: self.body.ids,
width: self.body.id_width as usize,
count: self.table.count,
count: self.table.count(),
}
}
/// Whether the ids index the executable's shared string table (and so
/// the VM-wide identifier slots).
/// Whether the ids index the executable's shared slot table (and so the
/// VM-wide identifier slots).
#[inline]
pub fn shared(&self) -> bool {
self.owned.is_none()
matches!(self.table, ModuleInfoStrings::Shared(_))
}
/// Ids below this are strings; the rest are sentinels.
#[inline]
pub fn strings_count(&self) -> usize {
self.table.count as usize
self.table.count() as usize
}
/// The WTF-8 bytes of string `id`, or `None` if out of bounds (corrupt input).
#[inline]
pub fn string(&self, id: u32) -> Option<&[u8]> {
pub fn string(&self, id: u32) -> Option<ModuleInfoString<'_>> {
self.table.get(id)
}

Expand All @@ -272,9 +297,11 @@ impl ModuleInfoDeserialized {
let (flags, body) = Body::parse(&bytes[table.byte_len..])?;
Ok(Box::new(ModuleInfoDeserialized {
body,
table,
table: ModuleInfoStrings::Owned {
table,
_bytes: owned,
},
flags,
owned: Some(owned),
}))
}

Expand All @@ -284,19 +311,18 @@ impl ModuleInfoDeserialized {
Self::create(source).ok()
}

/// A body inside the executable whose ids index the string table the
/// A body inside the executable whose ids index the slot table the
/// executable shares between all its modules (`StandaloneModuleGraph`).
/// Nothing is copied.
pub fn create_with_table(
table: &ModuleInfoStringTable<'static>,
table: &ModuleInfoSlotTable<'static>,
body: &'static [u8],
) -> Option<Box<ModuleInfoDeserialized>> {
let (flags, body) = Body::parse(body).ok()?;
Some(Box::new(ModuleInfoDeserialized {
body,
table: *table,
table: ModuleInfoStrings::Shared(*table),
flags,
owned: None,
}))
}
}
Expand Down Expand Up @@ -332,9 +358,8 @@ fn width(b: u8) -> Result<usize, ModuleInfoError> {
}
}

/// Borrowed view over a serialized
/// `bun_js_printer::analyze_transpiled_module::ModuleInfoStringTable`
/// (layout documented there).
/// Borrowed view over a self-contained record's string table
/// (`bun_js_printer::serialize_string_table`).
#[derive(Clone, Copy)]
pub struct ModuleInfoStringTable<'a> {
count: u32,
Expand Down Expand Up @@ -367,6 +392,9 @@ impl<'a> ModuleInfoStringTable<'a> {
.and_then(|n| n.checked_mul(offset_width))
.ok_or(ModuleInfoError::BadModuleInfo)?;
let offsets = r.bytes(offsets_len)?;
if !offsets_len.is_multiple_of(2) {
r.bytes(1)?;
}
let total = read_uint(&offsets[offsets_len - offset_width..], offset_width) as usize;
let buf = r.bytes(total)?;
Ok(Self {
Expand All @@ -392,9 +420,99 @@ impl<'a> ModuleInfoStringTable<'a> {
Some([start, end - start])
}
#[inline]
pub fn get(&self, id: u32) -> Option<&'a [u8]> {
pub fn get(&self, id: u32) -> Option<ModuleInfoString<'a>> {
let [offset, len] = self.range(id)?;
Some(&self.buf[offset as usize..(offset + len) as usize])
let (offset, end) = (offset as usize, (offset + len) as usize);
let (&tag, rest) = self.buf[offset..end].split_first()?;
match tag {
1 => Some(ModuleInfoString::Chars {
chars: rest,
is_8bit: true,
}),
0 => {
let chars = self.buf.get((offset + 2) & !1..end)?;
chars
.len()
.is_multiple_of(2)
.then_some(ModuleInfoString::Chars {
chars,
is_8bit: false,
})
}
_ => None,
}
}
}

/// The executable's module-info string table: `u32 count` then one `u32`
/// slot per string, each resolved by `JSC__IdentifierArray__setFromSlot`
/// exactly as the bytecode cache resolves its own string slots — up to three
/// Latin-1 characters inline (tag 1), an ordinal into the executable's
/// bytecode string table (tag 2), or the empty string (tag 3). Built by
/// `ModuleInfoSlotTableBuilder`.
#[derive(Clone, Copy)]
pub struct ModuleInfoSlotTable<'a> {
slots: &'a [u8],
}
impl<'a> ModuleInfoSlotTable<'a> {
pub fn parse(bytes: &'a [u8]) -> Result<Self, ModuleInfoError> {
if bytes.is_empty() {
return Ok(Self { slots: &[] });
}
let mut r = Reader { rem: bytes };
let count = r.u32()? as usize;
let slots = r.bytes(count.checked_mul(4).ok_or(ModuleInfoError::BadModuleInfo)?)?;
Ok(Self { slots })
}
#[inline]
pub fn count(&self) -> u32 {
(self.slots.len() / 4) as u32
}
#[inline]
pub fn get(&self, id: u32) -> Option<u32> {
let at = (id as usize).checked_mul(4)?;
let bytes = self.slots.get(at..at + 4)?;
Some(u32::from_le_bytes(bytes.try_into().unwrap()))
}
}

/// Interns every module's strings during a `--compile` link and serializes
/// the `ModuleInfoSlotTable` their bodies index.
#[derive(Default)]
pub struct ModuleInfoSlotTableBuilder {
ids: bun_collections::HashMap<Box<[u8]>, u32>,
slots: Vec<u32>,
}
impl ModuleInfoSlotTableBuilder {
/// Interns every string of `mi`; the result maps its local ids to table ids.
pub fn intern_all(&mut self, mi: &ModuleInfo, slot_for: impl Fn(&[u8]) -> u32) -> Vec<u32> {
let (strings_buf, strings_lens) = mi.strings();
let mut ids = Vec::with_capacity(strings_lens.len());
let mut offset = 0usize;
for &len in strings_lens {
let s = &strings_buf[offset..offset + len as usize];
offset += len as usize;
if let Some(&id) = self.ids.get(s) {
ids.push(id);
continue;
}
let id = u32::try_from(self.slots.len()).expect("int cast");
self.slots.push(slot_for(s));
self.ids.insert(s.into(), id);
ids.push(id);
}
ids
}
pub fn count(&self) -> u32 {
self.slots.len() as u32
}
pub fn serialize(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + self.slots.len() * 4);
out.extend_from_slice(&self.count().to_le_bytes());
for slot in &self.slots {
out.extend_from_slice(&slot.to_le_bytes());
}
out
}
}

Expand Down
Loading
Loading