Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
5 changes: 4 additions & 1 deletion src/ast/import_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ bitflags::bitflags! {
const WRAP_WITH_TO_ESM = 1 << 13;
const WRAP_WITH_TO_COMMONJS = 1 << 14;

// bit 15 (_padding: u1) intentionally unused
/// "import defer * as ns from 'path'" — defer evaluation of the
/// imported module until a property on the namespace object is
/// accessed. Requires `CONTAINS_IMPORT_STAR`.
const PHASE_DEFER = 1 << 15;
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/ast/s.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ pub struct Switch {
/// import * as ns from 'path'
/// import defaultItem, {item1, item2} from 'path'
/// import defaultItem, * as ns from 'path'
/// import defer * as ns from 'path'
///
/// Many parts are optional and can be combined in different ways. The only
/// restriction is that you cannot have both a clause and a star namespace.
Expand All @@ -207,6 +208,10 @@ pub struct Import {
pub star_name_loc: Option<crate::Loc>, // = None
pub import_record_index: u32,
pub is_single_line: bool, // = false
/// "import defer * as ns from 'path'" — the TC39 Deferred Module Evaluation
/// proposal. Only valid with a namespace import (`star_name_loc` is set,
/// `default_name`/`items` are empty).
pub phase_defer: bool, // = false
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we turn this into an enum of phases? to support source phase later

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reasonable — I went back and forth on this. A few notes on why I landed on the bool here but kept the enum where it matters for forward-compat:

  • The serialized ModuleInfo already uses a ModulePhase enum (Evaluation = 0, Defer = 1) for the per-requested-module phase byte, so the wire format is trivially extensible to Source = 2 without another cache-version bump.
  • import source x from "..." has a different grammar shape (default binding only, no * as), so it wouldn't share this S::Import branch in parse_stmt anyway — it'd get its own handling and probably its own S::Import field (or a shared phase: ImportPhase at that point).
  • ImportRecord::Flags is now full (u16, bit 15 is PHASE_DEFER). Adding PHASE_SOURCE would mean widening to u32 or moving phase to a separate u8 on the record — either is fine but felt like it belonged with the change that actually needs it.

Happy to switch phase_defer: boolphase: ImportPhase { Evaluation, Defer } on S::Import now if you'd rather have it in place — it's a small mechanical change. Let me know.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty reasonable - how can we make sure we are not introducing strange bugs by theoretically allowing an invalid state though?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The invariant (phase_deferstar_name_loc.is_some()default_name.is_none()items.is_empty()) is held by construction right now — the only place that sets phase_defer: true is the parser branch that builds a fresh S::Import { star_name_loc: Some(_), ..Default::default() }, and every other S::Import constructor (7 of them) explicitly passes phase_defer: false.

For defence-in-depth:

  • Printer already gates the defer token on PHASE_DEFER && CONTAINS_IMPORT_STAR, so even a malformed S::Import can't produce import defer"./x";.
  • scan_imports refuses to strip the star binding when phase_defer is set.
  • Added a debug_assert! in process_import_statement (1a98bc5) that checks the full shape invariant at the S::Import → import-record boundary, so any future producer that violates it trips immediately in debug builds rather than surfacing as odd output downstream.

An enum wouldn't actually prevent the cross-field invalid state (you could still have phase: ImportPhase::Defer with default_name: Some(_)), so the assert is the more direct guard. When import source lands and needs its own shape invariant (default-binding-only), that's the natural point to lift phase_defer: boolphase: ImportPhase and widen the record flags.

}

impl Default for Import {
Expand All @@ -218,6 +223,7 @@ impl Default for Import {
star_name_loc: None,
import_record_index: u32::MAX,
is_single_line: false,
phase_defer: false,
}
}
}
Expand Down
30 changes: 26 additions & 4 deletions src/bundler/analyze_transpiled_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use bun_core::{self, err, slice_as_bytes};
// the print boundary.
// ──────────────────────────────────────────────────────────────────────────
pub use bun_js_printer::analyze_transpiled_module::{
FetchParameters, ModuleInfo, StringID, VarKind,
FetchParameters, ModuleInfo, ModulePhase, StringID, VarKind,
};

/// Downstream name for `FetchParameters` — mirrors how
Expand Down Expand Up @@ -65,6 +65,8 @@ impl RecordKind {
pub const EXPORT_INFO_NAMESPACE: Self = Self(7);
/// module_name
pub const EXPORT_INFO_STAR: Self = Self(8);
/// module_name, import_name = '*', local_name (ModulePhase::Defer)
pub const IMPORT_INFO_NAMESPACE_DEFER: Self = Self(9);

// PascalCase aliases — `bundler_jsc::analyze_jsc` pattern-matches on these
// (the SCREAMING_CASE consts above are kept for intra-crate use).
Expand All @@ -73,6 +75,7 @@ impl RecordKind {
pub const ImportInfoSingle: Self = Self::IMPORT_INFO_SINGLE;
pub const ImportInfoSingleTypeScript: Self = Self::IMPORT_INFO_SINGLE_TYPE_SCRIPT;
pub const ImportInfoNamespace: Self = Self::IMPORT_INFO_NAMESPACE;
pub const ImportInfoNamespaceDefer: Self = Self::IMPORT_INFO_NAMESPACE_DEFER;
pub const ExportInfoIndirect: Self = Self::EXPORT_INFO_INDIRECT;
pub const ExportInfoLocal: Self = Self::EXPORT_INFO_LOCAL;
pub const ExportInfoNamespace: Self = Self::EXPORT_INFO_NAMESPACE;
Expand All @@ -84,6 +87,7 @@ impl RecordKind {
Self::IMPORT_INFO_SINGLE => Ok(3),
Self::IMPORT_INFO_SINGLE_TYPE_SCRIPT => Ok(3),
Self::IMPORT_INFO_NAMESPACE => Ok(3),
Self::IMPORT_INFO_NAMESPACE_DEFER => Ok(3),
Self::EXPORT_INFO_INDIRECT => Ok(3),
Self::EXPORT_INFO_LOCAL => Ok(3),
Self::EXPORT_INFO_NAMESPACE => Ok(2),
Expand Down Expand Up @@ -154,6 +158,7 @@ pub struct ModuleInfoDeserialized {
pub strings_lens: bun_ptr::RawSlice<u32>,
pub requested_modules_keys: bun_ptr::RawSlice<StringID>,
pub requested_modules_values: bun_ptr::RawSlice<FetchParameters>,
pub requested_modules_phases: bun_ptr::RawSlice<u8>,
pub buffer: bun_ptr::RawSlice<StringID>,
pub record_kinds: bun_ptr::RawSlice<RecordKind>,
pub flags: Flags,
Expand Down Expand Up @@ -200,6 +205,10 @@ impl ModuleInfoDeserialized {
self.requested_modules_values.slice()
}
#[inline]
pub fn requested_modules_phases(&self) -> &[u8] {
self.requested_modules_phases.slice()
}
#[inline]
pub fn buffer(&self) -> &[StringID] {
self.buffer.slice()
}
Expand Down Expand Up @@ -287,6 +296,8 @@ impl ModuleInfoDeserialized {
&mut rem,
requested_modules_len as usize * size_of::<FetchParameters>(),
)?)?;
let requested_modules_phases = Self::eat(&mut rem, requested_modules_len as usize)?;
let _ = Self::eat(&mut rem, ((4 - (requested_modules_len % 4)) % 4) as usize)?; // alignment padding

let flags = Flags::from_bits_retain(Self::eat_c::<1>(&mut rem)?[0]);
let _ = Self::eat(&mut rem, 3)?; // alignment padding
Expand All @@ -301,7 +312,7 @@ impl ModuleInfoDeserialized {
// Disarm the errdefer: ownership moves into the result.
let duped_raw = scopeguard::ScopeGuard::into_inner(guard);

// All six views borrow `duped_raw` (the boxed allocation moved into
// All seven views borrow `duped_raw` (the boxed allocation moved into
// `owner` below); they stay valid and at a stable address for the
// lifetime of every `RawSlice` copied from this struct. `RawSlice::new`
// erases the borrow lifetime — the structural invariant is upheld by
Expand All @@ -311,6 +322,7 @@ impl ModuleInfoDeserialized {
strings_lens: bun_ptr::RawSlice::new(strings_lens),
requested_modules_keys: bun_ptr::RawSlice::new(requested_modules_keys),
requested_modules_values: bun_ptr::RawSlice::new(requested_modules_values),
requested_modules_phases: bun_ptr::RawSlice::new(requested_modules_phases),
Comment thread
robobun marked this conversation as resolved.
buffer: bun_ptr::RawSlice::new(buffer),
record_kinds: bun_ptr::RawSlice::new(record_kinds),
flags,
Expand Down Expand Up @@ -347,6 +359,11 @@ impl ModuleInfoDeserialized {
writer.write_all(&(rm_keys.len() as u32).to_le_bytes())?;
writer.write_all(slice_as_bytes(rm_keys))?;
writer.write_all(slice_as_bytes(self.requested_modules_values()))?;
let rm_phases = self.requested_modules_phases();
debug_assert_eq!(rm_phases.len(), rm_keys.len());
writer.write_all(rm_phases)?;
let pad = (4 - (rm_keys.len() % 4)) % 4;
writer.write_all(&[0u8; 4][..pad])?; // alignment padding

writer.write_all(&[self.flags.bits()])?;
writer.write_all(&[0u8; 3])?; // alignment padding
Expand Down Expand Up @@ -469,13 +486,17 @@ impl ModuleInfoExt for ModuleInfo {
}
// PORT NOTE: reshaped for borrowck — capture lifetime-erased `RawSlice`
// views before `heap::into_raw(self)` consumes the box.
let (strings_buf, strings_lens, rm_keys, rm_values, buffer, record_kinds, flags);
let (strings_buf, strings_lens, rm_keys, rm_values, rm_phases, buffer, record_kinds, flags);
{
let view = self.as_deserialized();
strings_buf = bun_ptr::RawSlice::new(view.strings_buf);
strings_lens = bun_ptr::RawSlice::new(view.strings_lens);
rm_keys = bun_ptr::RawSlice::new(view.requested_modules_keys);
rm_values = bun_ptr::RawSlice::new(view.requested_modules_values);
// Printer's `ModulePhase` is `#[repr(u8)] NoUninit` — safe to view as `&[u8]`.
rm_phases = bun_ptr::RawSlice::new(bytemuck::cast_slice::<_, u8>(
view.requested_modules_phases,
));
buffer = bun_ptr::RawSlice::new(view.buffer);
// Printer's `RecordKind` is `#[repr(u8)] NoUninit` with the same
// discriminant layout as this crate's `#[repr(transparent)] u8`
Expand All @@ -488,14 +509,15 @@ impl ModuleInfoExt for ModuleInfo {
f.set(Flags::HAS_TLA, view.flags.has_tla);
flags = f;
}
// All six views point into the `Box<ModuleInfo>`'s vectors, moved into
// All seven views point into the `Box<ModuleInfo>`'s vectors, moved into
// `owner` below; they stay valid and stable for the lifetime of every
// `RawSlice` copied from this struct.
Box::new(ModuleInfoDeserialized {
strings_buf,
strings_lens,
requested_modules_keys: rm_keys,
requested_modules_values: rm_values,
requested_modules_phases: rm_phases,
buffer,
record_kinds,
flags,
Expand Down
Loading
Loading