Skip to content
38 changes: 22 additions & 16 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6621,7 +6621,20 @@ extern "C" JSC::EncodedJSValue Bun__REPL__evaluate(
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);

WTF::String source = WTF::String::fromUTF8(std::span { sourcePtr, sourceLen });
auto fail = [&](JSC::JSValue thrown) {
*exception = JSC::JSValue::encode(thrown);
// Set _error on the globalObject directly (not globalThis proxy)
Bun::putDirectNamed(vm, globalObject, "_error"_s, thrown);
scope.clearException();
return JSC::JSValue::encode(JSC::jsUndefined());
};

// WTF::String::fromUTF8 returns a null string for ill-formed UTF-8, and a null source evaluates to nothing.
WTF::String source = sourceLen > 0
? Zig::convertUTF8ToString(std::span { sourcePtr, sourceLen })
: WTF::emptyString();
if (source.isNull()) [[unlikely]]
return fail(JSC::createOutOfMemoryError(globalObject));
WTF::String filename = filenameLen > 0
? WTF::String::fromUTF8(std::span { filenamePtr, filenameLen })
: "[repl]"_s;
Expand All @@ -6639,21 +6652,11 @@ extern "C" JSC::EncodedJSValue Bun__REPL__evaluate(
WTF::NakedPtr<JSC::Exception> evalException;
JSC::JSValue result = JSC::evaluate(globalObject, sourceCode, globalObject->globalThis(), evalException);

if (evalException) {
*exception = JSC::JSValue::encode(evalException->value());
// Set _error on the globalObject directly (not globalThis proxy)
Bun::putDirectNamed(vm, globalObject, "_error"_s, evalException->value());
scope.clearException();
return JSC::JSValue::encode(JSC::jsUndefined());
}
if (evalException)
return fail(evalException->value());

if (scope.exception()) {
*exception = JSC::JSValue::encode(scope.exception()->value());
// Set _error on the globalObject directly (not globalThis proxy)
Bun::putDirectNamed(vm, globalObject, "_error"_s, scope.exception()->value());
scope.clearException();
return JSC::JSValue::encode(JSC::jsUndefined());
}
if (scope.exception())
return fail(scope.exception()->value());

// Note: _ is now set in src/runtime/cli/repl.rs after extracting the value
// from the REPL transform wrapper. We don't set it here anymore.
Expand Down Expand Up @@ -6691,7 +6694,10 @@ extern "C" JSC::EncodedJSValue Bun__REPL__getCompletions(

WTF::String prefix = prefixLen > 0
? WTF::String::fromUTF8(std::span { prefixPtr, prefixLen })
: WTF::String();
: WTF::emptyString();
// No name starts with a prefix that is not UTF-8.
if (prefix.isNull())
return JSC::JSValue::encode(JSC::jsUndefined());

// Walk the prototype chain as JSObject::getPropertyNames does (the builder dedups,
// the depth cap stops misbehaving proxy chains), but skip own index names where that
Expand Down
152 changes: 61 additions & 91 deletions src/runtime/cli/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use bun_sys::{self as sys, Fd};
// reference would violate provenance. This matches the convention in
// `src/jsc/lib.rs`.
unsafe extern "C" {
/// Start `exception` at `JSValue::ZERO`: a script can throw `undefined` or `null`.
fn Bun__REPL__evaluate(
globalObject: *const JSGlobalObject,
sourcePtr: *const u8,
Expand Down Expand Up @@ -945,6 +946,36 @@ impl Drawing {
}
}

/// What the REPL evaluates for one input.
enum ReplSource<'c> {
/// Output of the REPL transform: evaluates to a `{ value }` wrapper, or to a promise of one.
Transformed(Box<[u8]>),
/// The input as written, not taken by the transform: evaluates to its completion value.
AsWritten(&'c [u8]),
}

impl ReplSource<'_> {
fn bytes(&self) -> &[u8] {
match self {
ReplSource::Transformed(code) => code,
ReplSource::AsWritten(code) => code,
}
}

/// Only transform output is awaited. A promise from input as written is its value.
fn awaited_promise(&self, result: JSValue) -> Option<*mut jsc::JSPromise> {
match self {
ReplSource::Transformed(_) => result.as_promise(),
ReplSource::AsWritten(_) => None,
}
}

/// A completion value is never the `{ value }` wrapper, even when it has a `value` property.
fn is_value_wrapper(&self, result: JSValue) -> bool {
matches!(self, ReplSource::Transformed(_)) && result.is_object()
}
}

pub(super) struct Repl<'a> {
line_editor: LineEditor,
history: History,
Expand Down Expand Up @@ -1620,38 +1651,32 @@ impl<'a> Repl<'a> {
return;
};

// Transform the code using REPL mode (hoists declarations, wraps result in { value: expr })
let Some(transformed_code) = self.transform_for_repl(code) else {
// Transform failed, try evaluating raw code (for syntax errors, etc.)
self.evaluate_raw(code);
return;
};
let source = self.source_for_repl(code);

// Evaluate the transformed code
let mut exception: JSValue = JSValue::UNDEFINED;
let mut exception: JSValue = JSValue::ZERO;
// SAFETY: `global` is a live opaque `JSGlobalObject` handle; slice ptr/len pairs
// are valid for the duration of the call; `exception` is a stack local.
let result = unsafe {
Bun__REPL__evaluate(
global,
transformed_code.as_ptr(),
transformed_code.len(),
source.bytes().as_ptr(),
source.bytes().len(),
b"[repl]".as_ptr(),
b"[repl]".len(),
&raw mut exception,
)
};

// Check for exception
if !exception.is_undefined() && !exception.is_null() {
if !exception.is_empty() {
self.set_last_error(exception);
self.print_js_error(exception);
return;
}

// Handle async IIFE results - wait for promise to resolve
let mut resolved_result = result;
if let Some(promise) = result.as_promise() {
if let Some(promise) = source.awaited_promise(result) {
// Mark as handled BEFORE waiting to prevent unhandled rejection output
jsc::JSPromise::opaque_mut(promise).set_handled();

Expand Down Expand Up @@ -1697,7 +1722,7 @@ impl<'a> Repl<'a> {
// Extract the value from the result wrapper { value: expr }
// The REPL transform wraps the last expression in { value: expr }
let mut actual_result = resolved_result;
if resolved_result.is_object() {
if source.is_value_wrapper(resolved_result) {
// Wrapper is REPL-built { __proto__: null, value: ... } so getOwn shouldn't throw,
// but if it does, propagate as a REPL error.
let maybe_value =
Expand Down Expand Up @@ -1744,6 +1769,7 @@ impl<'a> Repl<'a> {
/// Uses the REPL transform pipeline (TypeScript/JSX, top-level await, object literal
/// wrapping, declaration hoisting), drains the event loop, and optionally prints the
/// result to stdout. Errors are written to stderr.
/// A script that the transform does not take is evaluated as written.
/// Returns true if an error occurred (the caller should set exit_code=1 and
/// skip onBeforeExit); false on success (caller preserves process.exitCode).
pub(super) fn eval_script(&mut self, code: &[u8], print_result: bool) -> bool {
Expand All @@ -1770,49 +1796,30 @@ impl<'a> Repl<'a> {
return false;
}

let Some(transformed_code) = self.transform_for_repl(code) else {
// Transform failed — fall back to raw evaluation for the error message
let mut exception: JSValue = JSValue::UNDEFINED;
// SAFETY: `global` is a live opaque `JSGlobalObject` handle; slice ptr/len pairs
// are valid for the duration of the call; `exception` is a stack local.
unsafe {
let _ = Bun__REPL__evaluate(
global,
code.as_ptr(),
code.len(),
b"[eval]".as_ptr(),
b"[eval]".len(),
&raw mut exception,
);
}
if !exception.is_undefined() && !exception.is_null() {
self.print_js_error_to(exception, Output::error_writer(), stderr_colors);
}
return true;
};
let source = self.source_for_repl(code);

let mut exception: JSValue = JSValue::UNDEFINED;
let mut exception: JSValue = JSValue::ZERO;
// SAFETY: `global` is a live opaque `JSGlobalObject` handle; slice ptr/len pairs
// are valid for the duration of the call; `exception` is a stack local.
let result = unsafe {
Bun__REPL__evaluate(
global,
transformed_code.as_ptr(),
transformed_code.len(),
source.bytes().as_ptr(),
source.bytes().len(),
b"[eval]".as_ptr(),
b"[eval]".len(),
&raw mut exception,
)
};

if !exception.is_undefined() && !exception.is_null() {
if !exception.is_empty() {
self.print_js_error_to(exception, Output::error_writer(), stderr_colors);
return true;
}

// If the transform wrapped in an async IIFE (top-level await), wait for it
let mut resolved_result = result;
if let Some(promise) = result.as_promise() {
if let Some(promise) = source.awaited_promise(result) {
// SAFETY: `promise` is a live JSC heap cell; `vm.jsc_vm` is the
// owning JSC VM handle for this thread.
jsc::JSPromise::opaque_mut(promise).set_handled();
Expand All @@ -1836,7 +1843,7 @@ impl<'a> Repl<'a> {

// Unwrap the { value: expr } wrapper produced by transform_for_repl
let mut actual_result = resolved_result;
if resolved_result.is_object() {
if source.is_value_wrapper(resolved_result) {
let maybe_value =
match resolved_result.get_own(global, &bun_core::String::static_("value")) {
Ok(v) => v,
Expand Down Expand Up @@ -1875,48 +1882,6 @@ impl<'a> Repl<'a> {
false
Comment thread
robobun marked this conversation as resolved.
Resolved
}

/// Evaluate code without REPL transforms (fallback for errors)
/// The C++ Bun__REPL__evaluate handles setting _ and _error
fn evaluate_raw(&mut self, code: &[u8]) {
let Some(global) = self.global else {
return;
};

let mut exception: JSValue = JSValue::UNDEFINED;
// SAFETY: `global` is a live opaque `JSGlobalObject` handle; slice ptr/len pairs
// are valid for the duration of the call; `exception` is a stack local.
let result = unsafe {
Bun__REPL__evaluate(
global,
code.as_ptr(),
code.len(),
b"[repl]".as_ptr(),
b"[repl]".len(),
&raw mut exception,
)
};

if !exception.is_undefined() && !exception.is_null() {
self.set_last_error(exception);
self.print_js_error(exception);
return;
}

self.set_last_result(result);

if !result.is_undefined() {
self.print_formatted_value(result);
} else if self.use_colors {
self.print(format_args!("{}undefined{}\n", Color::DIM, Color::RESET));
} else {
self.print(format_args!("undefined\n"));
}

if let Some(vm) = self.vm {
vm.as_mut().tick();
}
}

/// Evaluate code and copy the result to clipboard instead of printing it
fn evaluate_and_copy(&mut self, code: &[u8]) {
let Some(global) = self.global else {
Expand All @@ -1926,33 +1891,30 @@ impl<'a> Repl<'a> {
return;
};

let Some(transformed_code) = self.transform_for_repl(code) else {
self.evaluate_raw(code);
return;
};
let source = self.source_for_repl(code);

let mut exception: JSValue = JSValue::UNDEFINED;
let mut exception: JSValue = JSValue::ZERO;
// SAFETY: `global` is a live opaque `JSGlobalObject` handle; slice ptr/len pairs
// are valid for the duration of the call; `exception` is a stack local.
let result = unsafe {
Bun__REPL__evaluate(
global,
transformed_code.as_ptr(),
transformed_code.len(),
source.bytes().as_ptr(),
source.bytes().len(),
b"[repl]".as_ptr(),
b"[repl]".len(),
&raw mut exception,
)
};

if !exception.is_undefined() && !exception.is_null() {
if !exception.is_empty() {
self.set_last_error(exception);
self.print_js_error(exception);
return;
}

let mut resolved_result = result;
if let Some(promise) = result.as_promise() {
if let Some(promise) = source.awaited_promise(result) {
// SAFETY: `promise` is a live JSC heap cell; `vm.jsc_vm` is the
// owning JSC VM handle for this thread.
jsc::JSPromise::opaque_mut(promise).set_handled();
Expand Down Expand Up @@ -1984,7 +1946,7 @@ impl<'a> Repl<'a> {
}

let mut actual_result = resolved_result;
if resolved_result.is_object() {
if source.is_value_wrapper(resolved_result) {
let maybe_value =
match resolved_result.get_own(global, &bun_core::String::static_("value")) {
Ok(v) => v,
Expand Down Expand Up @@ -2111,6 +2073,14 @@ impl<'a> Repl<'a> {
Ok(())
}

/// Code that the transform does not take can be valid (a block, sloppy-mode syntax).
fn source_for_repl<'c>(&self, code: &'c [u8]) -> ReplSource<'c> {
match self.transform_for_repl(code) {
Some(transformed) => ReplSource::Transformed(transformed),
None => ReplSource::AsWritten(code),
}
}

/// Transform code using the REPL parser (hoists declarations, wraps expressions)
fn transform_for_repl(&self, code: &[u8]) -> Option<Box<[u8]>> {
let vm = self.vm?;
Expand Down
Loading
Loading