Compare commits

...

19 Commits

Author SHA1 Message Date
Claude Bot
aa5ea829a2 feat(repl): add shell command support and fix async event loop handling
- Add $`command` syntax for running shell commands in REPL
- Transform $`...` to await Bun.$`...` for shell execution
- Fix event loop handling for async operations by calling autoTick()
- Now properly awaits Bun.sleep(), setTimeout(), and Bun.$ operations
- Add tests for shell commands and Bun.sleep

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 22:11:14 +00:00
Claude Bot
4815c10efe fix(repl): address fourth round of CodeRabbit review comments
- Fix BracketDepth.template tracking: decrement template count when
  closing template literal and check template depth in needsContinuation()
- Fix reverseSearch: show proper "not yet implemented" message instead
  of confusing prompt that suggests waiting for input
- Fix evalDirect: await promises like eval() does for consistent async
  behavior
- Fix getHistoryPath: use std.fs.path.join instead of manual string
  formatting for cross-platform correctness

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 22:00:59 +00:00
Claude Bot
eb804bd8e0 fix more review feedback issues
- Fix History.next() underflow guard for empty history
- Fix saveHistory to resolve relative paths to absolute
- Fix completion suffix bounds check for property completions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 21:45:33 +00:00
Claude Bot
702e8448d6 address additional code review feedback
- Fix .install command prefix parsing (.i alias used wrong offset)
- Fix loadFile to resolve paths to absolute paths first
- Remove silent fallback in saveToFile, emit warning instead

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 21:26:55 +00:00
Claude Bot
be79d67802 address code review feedback for REPL
- Delete .bun_repl_history and add to .gitignore
- Fix memory leak in getCompletions for property names
- Use vm.waitForPromise instead of manual event loop polling
- Add proper cleanup for stores and arena on init failure
- Remove manual timeout from test helper

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 21:11:55 +00:00
autofix-ci[bot]
f5018d71df [autofix.ci] apply automated fixes 2026-01-20 20:53:57 +00:00
Claude Bot
cf159ae2fb test(repl): add comprehensive tests for bun repl
Add 41 tests covering:
- REPL commands (.exit, .q, .help)
- Expression evaluation (numbers, strings, objects, arrays)
- Variable persistence across lines
- Function and class definitions
- Async/await support
- Built-in APIs (Bun, process, fetch, URL, etc.)
- Error handling (syntax and runtime errors)
- Modern JS features (destructuring, spread, arrow functions, etc.)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 20:51:57 +00:00
Claude Bot
261f429fd1 feat(repl): add JSC-based autocomplete for object properties
The REPL now provides intelligent autocomplete for object properties
by dynamically querying the JSC runtime. When typing `obj.` and pressing
Tab, the REPL will show available properties from the actual object.

Features:
- Property completion for any object (e.g., `Bun.`, `console.`)
- Navigates nested paths (e.g., `Bun.file.`)
- Includes both own properties and prototype chain

Also adds tests for class persistence and destructuring to verify
the full AST transforms work correctly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 20:44:24 +00:00
Claude Bot
018358d8a1 feat(repl): persist history to ~/.bun_repl_history
The REPL now saves command history to the user's home directory
($HOME/.bun_repl_history or %USERPROFILE%\.bun_repl_history on Windows).

History is automatically loaded when the REPL starts and saved on exit.
The .save command can still be used to save history to a custom file.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 20:35:31 +00:00
Claude Bot
73cd4e92bb feat(repl): integrate full transpiler with replMode for AST transforms
This replaces the simple string-based let/const->var transform with
Bun's full transpiler in replMode, enabling:

- Proper hoisting of let/const/var/function/class declarations
- Top-level await support via async IIFE wrapping
- Result capture in { value: expr } wrapper
- Object literal disambiguation (wrapping { } in parens)
- Class and function declarations that persist across lines

The REPL now properly awaits promises by running the event loop
until they resolve, allowing `await Promise.resolve(42)` to work.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 20:28:02 +00:00
Claude Bot
e6f08624b1 test(repl): add comprehensive tests for bun repl
Add tests covering:
- Basic expression evaluation
- Bun globals availability
- Variable persistence (let, const, function)
- REPL commands (.help, .timing)
- Error handling
- Object and array display
- Multi-statement lines

Note: Top-level await test is skipped until the full REPL transforms
are integrated, which will wrap code containing await in an async IIFE.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 18:40:35 +00:00
Claude Bot
efa6eab698 feat(repl): add simple REPL transforms for let/const persistence
Add a simple string-based transform that converts top-level `let` and
`const` declarations to `var`, which allows variables to persist across
REPL lines since `var` declarations become properties of the global
object in sloppy mode.

This is a temporary solution until we can properly integrate the
AST-based repl_transforms.zig module with the transpiler pipeline.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 18:34:12 +00:00
Claude Bot
d0bf75f88f feat(repl): implement native Zig REPL for bun repl
This implements a new REPL in Zig that replaces the previous bunx-based
REPL. The new REPL provides:

- Interactive line editing with cursor movement and history navigation
- Syntax highlighting using QuickAndDirtyJavaScriptSyntaxHighlighter
- Multi-line input with bracket matching detection
- REPL commands (.help, .exit, .clear, .load, .save, .editor, .timing)
- JavaScript evaluation using the global eval() function
- Pretty-printed output for evaluated expressions
- Support for both TTY (interactive) and non-TTY (piped) input

The REPL properly initializes the JSC VirtualMachine with the API lock
held, enabling safe JavaScript execution. It uses Bun's transpiler
infrastructure and output formatting.

TODO:
- Implement REPL transforms for variable persistence across lines
- Integrate package installation
- Add shell mode integration
- Implement JSC-based autocomplete

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 18:26:35 +00:00
Jarred Sumner
3536e422e9 Merge branch 'main' into jarred/repl-mode 2026-01-19 14:09:41 -08:00
Jarred Sumner
8be7c161d0 fix: handle spread/rest patterns in convertBindingToExpr
- For arrays: wrap last element in E.Spread when has_spread is true
- For objects: set property.kind to .spread when is_spread flag is set
- Copy flags and is_single_line from binding to match Binding.toExpr

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# REPL Transform Fixes and Node.js Parity

## Current Status

The basic `replMode` option is implemented. This plan covers fixes and parity with Node.js REPL.

## Issues to Fix

### 1. Value Wrapper Has Extra Parentheses (CRITICAL)
**Current output:**
```js
({
  __proto__: null,
  value: 42
});
```

**Expected behavior (per Node.js):**
- For **non-async expressions**: Node.js returns `null` (no transform) - the REPL evaluates the expression directly
- For **async expressions**: `(async () => { return { __proto__: null, value: (expr) } })()`

**Solution:**
1. For non-async expressions: Don't wrap in `{ value: expr }` - just return the expression as-is
2. For async expressions: The `{ __proto__: null, value: expr }` is already inside the function after `return`, so no outer parens needed
3. Add inner parens around the expression value for clarity: `{ __proto__: null, value: (expr) }`

### 2. Object Literal Disambiguation (CRITICAL)
**Input:** `{a: 1}` or `{foo: await fetch()}`

**Current:** Parsed as block with labeled statement, NOT object literal

**Solution:** Pre-check input at transpiler layer:
- If code starts with `{` and doesn't end with `;`, try parsing as `(_=CODE)`
- If valid, wrap input as `(CODE)` before processing
- This matches Node.js approach in `repl.js` line 411-414

### 3. Class Declarations Don't Persist to VM Context
**Current:** Uses `let ClassName;` hoisting - doesn't become vm context property

**Node.js behavior:** Also uses `let` - this is a known limitation in Node.js too!

Looking at Node.js `await.js` line 31-37:
```js
ClassDeclaration(node, state, c) {
  state.prepend(node, `${node.id.name}=`);
  ArrayPrototypePush(state.hoistedDeclarationStatements, `let ${node.id.name}; `);
}
```

**Decision:** Use `var` instead of `let` for class hoisting. This makes classes persist to vm context, matching user expectations for REPL behavior. (Different from Node.js which uses `let`)

---

## Implementation Plan

## Usage Example

```typescript
// REPL tool implementation
const transpiler = new Bun.Transpiler({
  loader: "tsx",
  replMode: true,  // NEW OPTION
});

// For each REPL input line:
const transformed = transpiler.transformSync(userInput);

// Execute in persistent VM context
const result = vm.runInContext(transformed, replContext);

// result.value contains the expression result (wrapped to prevent auto-await)
console.log(result.value);
```

## Design Decisions

- **Value wrapper**: Use `{ value: expr }` wrapper like Node.js to prevent auto-awaiting Promise results
- **Static imports**: Keep static imports as-is (Bun handles them natively)
- **Scope**: Full Node.js REPL transform parity

---

## Node.js REPL Transform Behavior (Reference)

From `vendor/node/lib/internal/repl/await.js`:

### 1. Object Literal Detection
```javascript
// {a:1} → ({a:1}) when starts with { and no trailing ;
if (/^\s*{/.test(code) && !/;\s*$/.test(code)) {
  code = `(${code})`;
}
```

### 2. Top-Level Await Transform
```javascript
// Input:  await x
// Output: (async () => { return { value: (await x) } })()

// Input:  var x = await 1
// Output: var x; (async () => { void (x = await 1) })()

// Input:  const x = await 1
// Output: let x; (async () => { void (x = await 1) })()  // const→let

// Input:  function foo() {}  (with await somewhere)
// Output: var foo; (async () => { this.foo = foo; function foo() {} })()

// Input:  class Foo {}  (with await somewhere)
// Output: let Foo; (async () => { Foo=class Foo {} })()
```

### 3. Transform Skipping
Returns `null` (no transform) when:
- No `await` expression present at top level
- Top-level `return` statement exists
- Code is inside async functions/arrow functions/class methods

---

## Implementation Plan

### Fix 1: Remove Extra Parentheses from Value Wrapper

**Problem:** The printer adds `()` around objects at statement start to disambiguate from blocks.

**Solution:** Always use an IIFE wrapper (sync or async) so the object is after `return`:

```js
// Non-async expression (current - BAD)
({ __proto__: null, value: 42 });

// Non-async expression (fixed - GOOD)
(() => { return { __proto__: null, value: 42 } })()

// Non-async with hoisting (fixed - GOOD)
var x;
(() => { void (x = 1); return { __proto__: null, value: x } })()

// Async (already correct)
var x;
(async () => { void (x = await 1); return { __proto__: null, value: x } })()
```

**Files to modify:**
1. `src/ast/P.zig` - `applyReplValueWrapper()` function

**Changes:**
- Remove the simple `{ value: expr }` wrapper approach
- Always use `applyReplAsyncTransform()` style IIFE wrapping, but with `is_async = false` for non-async code
- This ensures the object is always after `return`, avoiding the parentheses issue
- Hoisting still works for both cases

### Fix 2: Object Literal Disambiguation

**File:** `src/bun.js/api/JSTranspiler.zig` - Before parsing

Add pre-processing check:
```zig
// In transformSync, before parsing:
if (config.repl_mode) {
    // Check if input looks like object literal: starts with { and doesn't end with ;
    if (startsWithBrace(source) and !endsWithSemicolon(source)) {
        // Try parsing as expression by wrapping: _=(CODE)
        // If valid, wrap input as (CODE)
        source = wrapAsExpression(source);
    }
}
```

This matches Node.js `isObjectLiteral()` check in `repl/utils.js:786-789`:
```js
function isObjectLiteral(code) {
  return /^\s*{/.test(code) && !/;\s*$/.test(code);
}
```

### Fix 3: Class Declaration Persistence

**File:** `src/ast/P.zig` - `applyReplAsyncTransform()` in the class handling section

Change from:
```zig
// let Foo; (hoisted)
try hoisted_stmts.append(p.s(S.Local{ .kind = .k_let, ... }));
```

To:
```zig
// var Foo; (hoisted) - use var so it becomes context property
try hoisted_stmts.append(p.s(S.Local{ .kind = .k_var, ... }));
// Also add: this.Foo = Foo; assignment after class declaration
```

---

## Node.js REPL Test Cases to Match

From `vendor/node/test/parallel/test-repl-preprocess-top-level-await.js`:

| Input | Expected Output |
|-------|-----------------|
| `await 0` | `(async () => { return { value: (await 0) } })()` |
| `var a = await 1` | `var a; (async () => { void (a = await 1) })()` |
| `let a = await 1` | `let a; (async () => { void (a = await 1) })()` |
| `const a = await 1` | `let a; (async () => { void (a = await 1) })()` |
| `await 0; function foo() {}` | `var foo; (async () => { await 0; this.foo = foo; function foo() {} })()` |
| `await 0; class Foo {}` | `let Foo; (async () => { await 0; Foo=class Foo {} })()` |
| `var {a} = {a:1}, [b] = [1]` | `var a, b; (async () => { void ( ({a} = {a:1}), ([b] = [1])) })()` |

---

## Files to Modify

| File | Changes |
|------|---------|
| `src/ast/P.zig` | Fix value wrapper format, fix class hoisting to use var |
| `src/bun.js/api/JSTranspiler.zig` | Add object literal pre-check |
| `src/ast/js_printer.zig` | May need to check object literal printing |
| `test/js/bun/transpiler/repl-transform.test.ts` | Update tests for exact Node.js parity |

---

## Verification

1. Run Node.js preprocess test cases through Bun's transpiler
2. Verify output matches Node.js exactly (or functionally equivalent)
3. Test with vm.runInContext for variable persistence
4. Test object literal inputs: `{a: 1}`, `{foo: await bar()}`

---

## DEPRECATED - Previous Implementation (Already Done)

### 1. Add `replMode` to Bun.Transpiler API

**File**: `src/bun.js/api/JSTranspiler.zig`

Add to `Config` struct (around line 27-44):
```zig
pub const Config = struct {
    // ... existing fields ...
    repl_mode: bool = false,
```

Parse the option in `Config.fromJS()` (around line 420-430):
```zig
if (try object.getBooleanLoose(globalThis, "replMode")) |flag| {
    this.repl_mode = flag;
}
```

Apply the option in `constructor()` (around line 714-721):
```zig
transpiler.options.repl_mode = config.repl_mode;
```

### 2. Add Feature Flag to Runtime

**File**: `src/runtime.zig` (in `Runtime.Features`)

```zig
/// REPL mode: transforms code for interactive evaluation
/// - Wraps lone object literals `{...}` in parentheses
/// - Hoists variable declarations for REPL persistence
/// - Wraps last expression in { value: expr } for result capture
/// - Assigns functions to context for persistence
repl_mode: bool = false,
```

### 3. Add to BundleOptions

**File**: `src/options.zig`

Add to `BundleOptions` struct:
```zig
repl_mode: bool = false,
```

### 4. Implement REPL Transforms in Parser

**File**: `src/ast/P.zig`

#### 4a. Object Literal Detection (Parser-Level)

In REPL mode, the parser should prefer interpreting ambiguous `{...}` as object literals instead of blocks.

**Location**: `src/ast/parseStmt.zig` in statement parsing

When `repl_mode` is true and the parser sees `{` at the start of a statement:
1. Try parsing as expression statement (object literal) first
2. If that fails, fall back to block statement

This is similar to how JavaScript engines handle REPL input. The parser already has the infrastructure to do this - we just need to change the precedence in REPL mode.

```zig
// In parseStmt when repl_mode is true and we see '{'
if (p.options.features.repl_mode and p.token.tag == .t_open_brace) {
    // Try parsing as expression first
    const saved_state = p.saveState();
    if (p.tryParseExpressionStatement()) |expr_stmt| {
        return expr_stmt;
    }
    p.restoreState(saved_state);
    // Fall back to block statement
    return p.parseBlockStatement();
}
```

This handles:
- `{a: 1}` → parsed as object literal expression
- `{a: 1, b: 2}` → parsed as object literal expression
- `{ let x = 1; }` → fails as expression, parsed as block
- `{ label: break label; }` → fails as expression (break not valid in object), parsed as block

#### 4b. REPL Transform Pass (in toAST after visiting)

Add a new function `applyReplTransforms()` that:

1. **Detect if transform is needed**: Walk AST to check for top-level `await`
2. **Skip transform when**:
   - No `await` at top level
   - Top-level `return` statement exists
3. **When transform IS needed**:
   - Wrap entire code in `(async () => { ... })()`
   - Hoist variable declarations outside the async wrapper
   - Convert `const` to `let` for persistence
   - Wrap last expression in `return { value: (expr) }`
   - Handle function declarations (assign to `this`)
   - Handle class declarations (hoist as `let`)

**Key Logic:**
```zig
fn applyReplTransforms(p: *Parser, stmts: []Stmt) ![]Stmt {
    // 1. Check for top-level await
    const has_await = p.hasTopLevelAwait(stmts);
    const has_return = p.hasTopLevelReturn(stmts);

    if (!has_await or has_return) {
        // Just wrap last expression, no async wrapper needed
        return p.wrapLastExpression(stmts);
    }

    // 2. Collect declarations to hoist
    var hoisted = std.ArrayList(Stmt).init(p.allocator);
    var inner_stmts = std.ArrayList(Stmt).init(p.allocator);

    for (stmts) |stmt| {
        switch (stmt.data) {
            .s_local => |local| {
                // Hoist declaration, convert const→let
                try hoisted.append(p.createHoistedDecl(local));
                // Add assignment expression to inner
                try inner_stmts.append(p.createAssignmentExpr(local));
            },
            .s_function => |func| {
                // var foo; (hoisted)
                try hoisted.append(p.createVarDecl(func.name));
                // this.foo = foo; function foo() {} (inner)
                try inner_stmts.append(p.createThisAssignment(func.name));
                try inner_stmts.append(stmt);
            },
            .s_class => |class| {
                // let Foo; (hoisted)
                try hoisted.append(p.createLetDecl(class.name));
                // Foo = class Foo {} (inner)
                try inner_stmts.append(p.createClassAssignment(class));
            },
            else => try inner_stmts.append(stmt),
        }
    }

    // 3. Wrap last expression in return { value: expr }
    p.wrapLastExpressionWithReturn(&inner_stmts);

    // 4. Create async IIFE: (async () => { ...inner... })()
    const async_iife = p.createAsyncIIFE(inner_stmts.items);

    // 5. Combine: hoisted declarations + async IIFE
    try hoisted.append(async_iife);
    return hoisted.toOwnedSlice();
}
```

### 5. TypeScript Type Definitions

**File**: `packages/bun-types/bun.d.ts`

Add to `TranspilerOptions` interface (around line 1748):
```typescript
interface TranspilerOptions {
  // ... existing options ...

  /**
   * Enable REPL mode transforms:
   * - Wraps object literals in parentheses
   * - Hoists declarations for REPL persistence
   * - Wraps last expression in { value: expr } for result capture
   * - Wraps code with await in async IIFE
   */
  replMode?: boolean;
}
```

---

## Files to Modify

| File | Changes |
|------|---------|
| `src/bun.js/api/JSTranspiler.zig` | Add `repl_mode` to Config, parse from JS, apply to transpiler |
| `src/runtime.zig` | Add `repl_mode: bool` to `Runtime.Features` |
| `src/options.zig` | Add `repl_mode: bool` to `BundleOptions` |
| `src/ast/P.zig` | REPL transform pass in `toAST()` |
| `src/ast/parseStmt.zig` | Object literal vs block disambiguation in REPL mode |
| `packages/bun-types/bun.d.ts` | Add `replMode?: boolean` to `TranspilerOptions` |

---

## Test Cases

Create test file: `test/js/bun/transpiler/repl-transform.test.ts`

### Part 1: Transform Output Tests (Unit Tests)

Test exact transformation output matches expected patterns:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("Bun.Transpiler replMode - Transform Output", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  // Based on Node.js test-repl-preprocess-top-level-await.js
  const testCases: [string, string | null][] = [
    // No await = null (no async transform, but still expression capture)
    ['0', null],

    // Basic await
    ['await 0', '(async () => { return { value: (await 0) } })()'],
    ['await 0;', '(async () => { return { value: (await 0) }; })()'],
    ['(await 0)', '(async () => { return ({ value: (await 0) }) })()'],

    // No transform for await inside async functions
    ['async function foo() { await 0; }', null],
    ['async () => await 0', null],
    ['class A { async method() { await 0 } }', null],

    // Top-level return = no transform
    ['await 0; return 0;', null],

    // Multiple await - last one gets return wrapper
    ['await 1; await 2;', '(async () => { await 1; return { value: (await 2) }; })()'],

    // Variable hoisting - var
    ['var a = await 1', 'var a; (async () => { void (a = await 1) })()'],

    // Variable hoisting - let
    ['let a = await 1', 'let a; (async () => { void (a = await 1) })()'],

    // Variable hoisting - const becomes let
    ['const a = await 1', 'let a; (async () => { void (a = await 1) })()'],

    // For loop with var - hoist var
    ['for (var i = 0; i < 1; ++i) { await i }',
     'var i; (async () => { for (void (i = 0); i < 1; ++i) { await i } })()'],

    // For loop with let - no hoist
    ['for (let i = 0; i < 1; ++i) { await i }',
     '(async () => { for (let i = 0; i < 1; ++i) { await i } })()'],

    // Destructuring with var
    ['var {a} = {a:1}, [b] = [1], {c:{d}} = {c:{d: await 1}}',
     'var a, b, d; (async () => { void ( ({a} = {a:1}), ([b] = [1]), ({c:{d}} = {c:{d: await 1}})) })()'],

    // Destructuring with let
    ['let [a, b, c] = await ([1, 2, 3])',
     'let a, b, c; (async () => { void ([a, b, c] = await ([1, 2, 3])) })()'],

    // Function declarations - assign to this
    ['await 0; function foo() {}',
     'var foo; (async () => { await 0; this.foo = foo; function foo() {} })()'],

    // Class declarations - hoist as let
    ['await 0; class Foo {}',
     'let Foo; (async () => { await 0; Foo=class Foo {} })()'],

    // Nested scopes
    ['if (await true) { var a = 1; }',
     'var a; (async () => { if (await true) { void (a = 1); } })()'],
    ['if (await true) { let a = 1; }',
     '(async () => { if (await true) { let a = 1; } })()'],

    // Mixed declarations
    ['var a = await 1; let b = 2; const c = 3;',
     'var a; let b; let c; (async () => { void (a = await 1); void (b = 2); void (c = 3); })()'],

    // for await
    ['for await (var i of asyncIterable) { i; }',
     'var i; (async () => { for await (i of asyncIterable) { i; } })()'],

    // for-of with var
    ['for (var i of [1,2,3]) { await 1; }',
     'var i; (async () => { for (i of [1,2,3]) { await 1; } })()'],

    // for-in with var
    ['for (var i in {x:1}) { await 1 }',
     'var i; (async () => { for (i in {x:1}) { await 1 } })()'],

    // Spread in destructuring
    ['var { ...rest } = await {}',
     'var rest; (async () => { void ({ ...rest } = await {}) })()'],
  ];

  for (const [input, expected] of testCases) {
    test(`transform: ${input.slice(0, 40)}...`, () => {
      const result = transpiler.transformSync(input);
      if (expected === null) {
        // No async transform expected, but expression capture may still happen
        expect(result).not.toMatch(/^\(async/);
      } else {
        expect(result.trim()).toBe(expected);
      }
    });
  }

  // Object literal detection - parser handles this automatically in REPL mode
  describe("object literal vs block disambiguation", () => {
    test("{a: 1} parsed as object literal", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{a: 1}");
      const result = await vm.runInContext(code, ctx);
      // Should evaluate to object, not undefined (block with label)
      expect(result.value).toEqual({ a: 1 });
    });

    test("{a: 1, b: 2} parsed as object literal", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{a: 1, b: 2}");
      const result = await vm.runInContext(code, ctx);
      expect(result.value).toEqual({ a: 1, b: 2 });
    });

    test("{ let x = 1; x } parsed as block", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{ let x = 1; x }");
      const result = await vm.runInContext(code, ctx);
      // Block returns last expression value
      expect(result.value).toBe(1);
    });

    test("{ x: 1; y: 2 } parsed as block with labels", async () => {
      // Semicolons make this a block with labeled statements, not object
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{ x: 1; y: 2 }");
      const result = await vm.runInContext(code, ctx);
      // Block with labels returns last value
      expect(result.value).toBe(2);
    });
  });
});
```

### Part 2: Variable Persistence Tests (Integration with node:vm)

Test that variables persist across multiple REPL evaluations:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL Variable Persistence", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  // Helper to run multiple REPL lines in sequence
  async function runReplSession(lines: string[], context?: object) {
    const ctx = vm.createContext(context ?? { console });
    const results: any[] = [];

    for (const line of lines) {
      const transformed = transpiler.transformSync(line);
      const result = await vm.runInContext(transformed, ctx);
      results.push(result?.value ?? result);
    }

    return { results, context: ctx };
  }

  test("var persists across lines", async () => {
    const { results, context } = await runReplSession([
      "var x = 10",
      "x + 5",
      "x = 20",
      "x",
    ]);

    expect(results[1]).toBe(15);  // x + 5
    expect(results[3]).toBe(20);  // x after reassignment
    expect(context.x).toBe(20);   // x visible in context
  });

  test("let persists across lines (hoisted)", async () => {
    const { results } = await runReplSession([
      "let y = await Promise.resolve(100)",
      "y * 2",
    ]);

    expect(results[1]).toBe(200);
  });

  test("const becomes let, can be reassigned in later lines", async () => {
    const { results } = await runReplSession([
      "const z = await Promise.resolve(5)",
      "z",
      // In REPL, const becomes let, so next line can redeclare
      "z = 10",  // This works because const→let
      "z",
    ]);

    expect(results[1]).toBe(5);
    expect(results[3]).toBe(10);
  });

  test("function declarations persist", async () => {
    const { results, context } = await runReplSession([
      "await 1; function add(a, b) { return a + b; }",
      "add(2, 3)",
      "function multiply(a, b) { return a * b; }",  // no await
      "multiply(4, 5)",
    ]);

    expect(results[1]).toBe(5);
    expect(results[3]).toBe(20);
    expect(typeof context.add).toBe("function");
    expect(typeof context.multiply).toBe("function");
  });

  test("class declarations persist", async () => {
    const { results, context } = await runReplSession([
      "await 1; class Counter { constructor() { this.count = 0; } inc() { this.count++; } }",
      "const c = new Counter()",
      "c.inc(); c.inc(); c.count",
    ]);

    expect(results[2]).toBe(2);
    expect(typeof context.Counter).toBe("function");
  });

  test("complex session with mixed declarations", async () => {
    const { results } = await runReplSession([
      "var total = 0",
      "async function addAsync(n) { return total += await Promise.resolve(n); }",
      "await addAsync(10)",
      "await addAsync(20)",
      "total",
    ]);

    expect(results[2]).toBe(10);
    expect(results[3]).toBe(30);
    expect(results[4]).toBe(30);
  });

  test("destructuring assignment persists", async () => {
    const { results, context } = await runReplSession([
      "var { a, b } = await Promise.resolve({ a: 1, b: 2 })",
      "a + b",
      "var [x, y, z] = [10, 20, 30]",
      "x + y + z",
    ]);

    expect(results[1]).toBe(3);
    expect(results[3]).toBe(60);
    expect(context.a).toBe(1);
    expect(context.x).toBe(10);
  });
});
```

### Part 3: eval() Scoping Semantics Tests

Test that REPL behaves like eval() with proper scoping:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL eval() Scoping Semantics", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  test("var hoists to global context", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("var globalVar = 42");
    await vm.runInContext(code, ctx);

    expect(ctx.globalVar).toBe(42);
  });

  test("let/const hoisted for REPL but scoped correctly", async () => {
    const ctx = vm.createContext({});

    // With await, let is hoisted outside async wrapper
    const code1 = transpiler.transformSync("let x = await 1");
    await vm.runInContext(code1, ctx);
    expect(ctx.x).toBe(1);

    // Without await, let behavior depends on implementation
    const code2 = transpiler.transformSync("let y = 2");
    await vm.runInContext(code2, ctx);
    // y should still be accessible in REPL context
  });

  test("block-scoped let does NOT leak", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("if (await true) { let blockScoped = 1; }");
    await vm.runInContext(code, ctx);

    // blockScoped should NOT be visible in context
    expect(ctx.blockScoped).toBeUndefined();
  });

  test("function in block hoists with var (sloppy mode)", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("if (await true) { function blockFn() { return 42; } }");
    await vm.runInContext(code, ctx);

    // In sloppy mode, function in block hoists to function scope
    expect(typeof ctx.blockFn).toBe("function");
    expect(ctx.blockFn()).toBe(42);
  });

  test("this binding in function declarations", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("await 1; function greet() { return 'hello'; }");
    await vm.runInContext(code, ctx);

    // Function should be assigned to this (context) for REPL persistence
    expect(ctx.greet()).toBe("hello");
  });

  test("async function expression captures result", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("await (async () => { return 42; })()");
    const result = await vm.runInContext(code, ctx);

    expect(result.value).toBe(42);
  });

  test("Promise result NOT auto-awaited due to { value: } wrapper", async () => {
    const ctx = vm.createContext({});

    // Without wrapper, result would be auto-awaited
    // With { value: } wrapper, we get the Promise object
    const code = transpiler.transformSync("await Promise.resolve(Promise.resolve(42))");
    const result = await vm.runInContext(code, ctx);

    // The inner Promise should be in value, not auto-resolved
    expect(result.value).toBeInstanceOf(Promise);
    expect(await result.value).toBe(42);
  });
});
```

### Part 4: Edge Cases and Error Handling

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL Edge Cases", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  test("empty input", () => {
    const result = transpiler.transformSync("");
    expect(result).toBe("");
  });

  test("whitespace only", () => {
    const result = transpiler.transformSync("   \n\t  ");
    expect(result.trim()).toBe("");
  });

  test("comment only", () => {
    const result = transpiler.transformSync("// just a comment");
    expect(result).toContain("// just a comment");
  });

  test("multiline input", () => {
    const input = `
      var x = await 1;
      var y = await 2;
      x + y
    `;
    const result = transpiler.transformSync(input);
    expect(result).toContain("var x");
    expect(result).toContain("var y");
    expect(result).toContain("async");
  });

  test("TypeScript syntax", () => {
    const input = "const x: number = await Promise.resolve(42)";
    const result = transpiler.transformSync(input);
    expect(result).not.toContain(": number"); // Types stripped
    expect(result).toContain("let x");
  });

  test("JSX in REPL", () => {
    const input = "await Promise.resolve(<div>Hello</div>)";
    const result = transpiler.transformSync(input);
    expect(result).toContain("async");
  });

  test("import expression (dynamic)", () => {
    // Dynamic imports should work fine
    const input = "await import('fs')";
    const result = transpiler.transformSync(input);
    expect(result).toContain("import");
  });

  test("nested await expressions", () => {
    const input = "await (await Promise.resolve(Promise.resolve(1)))";
    const result = transpiler.transformSync(input);
    expect(result).toContain("{ value:");
  });

  test("for-await-of", () => {
    const input = `
      async function* gen() { yield 1; yield 2; }
      for await (const x of gen()) { console.log(x); }
    `;
    const result = transpiler.transformSync(input);
    expect(result).toContain("var gen");
    expect(result).toContain("for await");
  });
});
```

---

## Verification Plan

### 1. Build and Basic Tests
```bash
# Build Bun with changes
bun bd

# Run the REPL transform tests
bun bd test test/js/bun/transpiler/repl-transform.test.ts
```

### 2. Manual Transform Output Verification
```typescript
// test-repl-manual.ts
const t = new Bun.Transpiler({ loader: "tsx", replMode: true });

// Object literal
console.log("Object literal:");
console.log(t.transformSync("{a: 1}"));
// Expected: contains "({a: 1})"

// Basic await
console.log("\nBasic await:");
console.log(t.transformSync("await 0"));
// Expected: (async () => { return { value: (await 0) } })()

// Variable hoisting
console.log("\nVar hoisting:");
console.log(t.transformSync("var x = await 1"));
// Expected: var x; (async () => { void (x = await 1) })()

// const → let
console.log("\nConst to let:");
console.log(t.transformSync("const x = await 1"));
// Expected: let x; (async () => { void (x = await 1) })()

// Function hoisting
console.log("\nFunction:");
console.log(t.transformSync("await 0; function foo() {}"));
// Expected: var foo; (async () => { await 0; this.foo = foo; function foo() {} })()
```

### 3. Full REPL Session Simulation
```typescript
// test-repl-session.ts
import vm from "node:vm";

const t = new Bun.Transpiler({ loader: "tsx", replMode: true });
const ctx = vm.createContext({ console, Promise });

async function repl(code: string) {
  const transformed = t.transformSync(code);
  console.log(`> ${code}`);
  console.log(`[transformed]: ${transformed}`);
  const result = await vm.runInContext(transformed, ctx);
  console.log(`= ${JSON.stringify(result?.value ?? result)}\n`);
  return result?.value ?? result;
}

// Test session
await repl("var counter = 0");
await repl("function increment() { return ++counter; }");
await repl("increment()");  // Should be 1
await repl("increment()");  // Should be 2
await repl("counter");      // Should be 2

await repl("const data = await Promise.resolve({ x: 10, y: 20 })");
await repl("data.x + data.y");  // Should be 30

await repl("class Point { constructor(x, y) { this.x = x; this.y = y; } }");
await repl("const p = new Point(3, 4)");
await repl("Math.sqrt(p.x**2 + p.y**2)");  // Should be 5
```

### 4. Verify No Regressions
```bash
# Run existing transpiler tests
bun bd test test/js/bun/transpiler/

# Run existing vm tests
bun bd test test/js/node/vm/
```

### 5. Cross-check with Node.js (Optional)
Compare transform outputs with Node.js's `processTopLevelAwait`:
```typescript
// Compare a few key transforms with Node.js output
const cases = [
  "await 0",
  "var x = await 1",
  "await 0; function foo() {}",
];
// Verify Bun output matches Node.js patterns
```
</claude-plan>
2026-01-19 11:07:58 -08:00
Jarred Sumner
a47f1555d6 refactor: address code review feedback
- Clarify JSDoc for replMode in bun.d.ts
- Consolidate applySyncTransform and applyAsyncTransform into transformWithHoisting
- Fix b_missing to return E.Missing in createBindingAssignment
- Add documentation comment for repl_mode in options.zig
- Remove unnecessary async modifiers from sync test callbacks

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# REPL Transform Fixes and Node.js Parity

## Current Status

The basic `replMode` option is implemented. This plan covers fixes and parity with Node.js REPL.

## Issues to Fix

### 1. Value Wrapper Has Extra Parentheses (CRITICAL)
**Current output:**
```js
({
  __proto__: null,
  value: 42
});
```

**Expected behavior (per Node.js):**
- For **non-async expressions**: Node.js returns `null` (no transform) - the REPL evaluates the expression directly
- For **async expressions**: `(async () => { return { __proto__: null, value: (expr) } })()`

**Solution:**
1. For non-async expressions: Don't wrap in `{ value: expr }` - just return the expression as-is
2. For async expressions: The `{ __proto__: null, value: expr }` is already inside the function after `return`, so no outer parens needed
3. Add inner parens around the expression value for clarity: `{ __proto__: null, value: (expr) }`

### 2. Object Literal Disambiguation (CRITICAL)
**Input:** `{a: 1}` or `{foo: await fetch()}`

**Current:** Parsed as block with labeled statement, NOT object literal

**Solution:** Pre-check input at transpiler layer:
- If code starts with `{` and doesn't end with `;`, try parsing as `(_=CODE)`
- If valid, wrap input as `(CODE)` before processing
- This matches Node.js approach in `repl.js` line 411-414

### 3. Class Declarations Don't Persist to VM Context
**Current:** Uses `let ClassName;` hoisting - doesn't become vm context property

**Node.js behavior:** Also uses `let` - this is a known limitation in Node.js too!

Looking at Node.js `await.js` line 31-37:
```js
ClassDeclaration(node, state, c) {
  state.prepend(node, `${node.id.name}=`);
  ArrayPrototypePush(state.hoistedDeclarationStatements, `let ${node.id.name}; `);
}
```

**Decision:** Use `var` instead of `let` for class hoisting. This makes classes persist to vm context, matching user expectations for REPL behavior. (Different from Node.js which uses `let`)

---

## Implementation Plan

## Usage Example

```typescript
// REPL tool implementation
const transpiler = new Bun.Transpiler({
  loader: "tsx",
  replMode: true,  // NEW OPTION
});

// For each REPL input line:
const transformed = transpiler.transformSync(userInput);

// Execute in persistent VM context
const result = vm.runInContext(transformed, replContext);

// result.value contains the expression result (wrapped to prevent auto-await)
console.log(result.value);
```

## Design Decisions

- **Value wrapper**: Use `{ value: expr }` wrapper like Node.js to prevent auto-awaiting Promise results
- **Static imports**: Keep static imports as-is (Bun handles them natively)
- **Scope**: Full Node.js REPL transform parity

---

## Node.js REPL Transform Behavior (Reference)

From `vendor/node/lib/internal/repl/await.js`:

### 1. Object Literal Detection
```javascript
// {a:1} → ({a:1}) when starts with { and no trailing ;
if (/^\s*{/.test(code) && !/;\s*$/.test(code)) {
  code = `(${code})`;
}
```

### 2. Top-Level Await Transform
```javascript
// Input:  await x
// Output: (async () => { return { value: (await x) } })()

// Input:  var x = await 1
// Output: var x; (async () => { void (x = await 1) })()

// Input:  const x = await 1
// Output: let x; (async () => { void (x = await 1) })()  // const→let

// Input:  function foo() {}  (with await somewhere)
// Output: var foo; (async () => { this.foo = foo; function foo() {} })()

// Input:  class Foo {}  (with await somewhere)
// Output: let Foo; (async () => { Foo=class Foo {} })()
```

### 3. Transform Skipping
Returns `null` (no transform) when:
- No `await` expression present at top level
- Top-level `return` statement exists
- Code is inside async functions/arrow functions/class methods

---

## Implementation Plan

### Fix 1: Remove Extra Parentheses from Value Wrapper

**Problem:** The printer adds `()` around objects at statement start to disambiguate from blocks.

**Solution:** Always use an IIFE wrapper (sync or async) so the object is after `return`:

```js
// Non-async expression (current - BAD)
({ __proto__: null, value: 42 });

// Non-async expression (fixed - GOOD)
(() => { return { __proto__: null, value: 42 } })()

// Non-async with hoisting (fixed - GOOD)
var x;
(() => { void (x = 1); return { __proto__: null, value: x } })()

// Async (already correct)
var x;
(async () => { void (x = await 1); return { __proto__: null, value: x } })()
```

**Files to modify:**
1. `src/ast/P.zig` - `applyReplValueWrapper()` function

**Changes:**
- Remove the simple `{ value: expr }` wrapper approach
- Always use `applyReplAsyncTransform()` style IIFE wrapping, but with `is_async = false` for non-async code
- This ensures the object is always after `return`, avoiding the parentheses issue
- Hoisting still works for both cases

### Fix 2: Object Literal Disambiguation

**File:** `src/bun.js/api/JSTranspiler.zig` - Before parsing

Add pre-processing check:
```zig
// In transformSync, before parsing:
if (config.repl_mode) {
    // Check if input looks like object literal: starts with { and doesn't end with ;
    if (startsWithBrace(source) and !endsWithSemicolon(source)) {
        // Try parsing as expression by wrapping: _=(CODE)
        // If valid, wrap input as (CODE)
        source = wrapAsExpression(source);
    }
}
```

This matches Node.js `isObjectLiteral()` check in `repl/utils.js:786-789`:
```js
function isObjectLiteral(code) {
  return /^\s*{/.test(code) && !/;\s*$/.test(code);
}
```

### Fix 3: Class Declaration Persistence

**File:** `src/ast/P.zig` - `applyReplAsyncTransform()` in the class handling section

Change from:
```zig
// let Foo; (hoisted)
try hoisted_stmts.append(p.s(S.Local{ .kind = .k_let, ... }));
```

To:
```zig
// var Foo; (hoisted) - use var so it becomes context property
try hoisted_stmts.append(p.s(S.Local{ .kind = .k_var, ... }));
// Also add: this.Foo = Foo; assignment after class declaration
```

---

## Node.js REPL Test Cases to Match

From `vendor/node/test/parallel/test-repl-preprocess-top-level-await.js`:

| Input | Expected Output |
|-------|-----------------|
| `await 0` | `(async () => { return { value: (await 0) } })()` |
| `var a = await 1` | `var a; (async () => { void (a = await 1) })()` |
| `let a = await 1` | `let a; (async () => { void (a = await 1) })()` |
| `const a = await 1` | `let a; (async () => { void (a = await 1) })()` |
| `await 0; function foo() {}` | `var foo; (async () => { await 0; this.foo = foo; function foo() {} })()` |
| `await 0; class Foo {}` | `let Foo; (async () => { await 0; Foo=class Foo {} })()` |
| `var {a} = {a:1}, [b] = [1]` | `var a, b; (async () => { void ( ({a} = {a:1}), ([b] = [1])) })()` |

---

## Files to Modify

| File | Changes |
|------|---------|
| `src/ast/P.zig` | Fix value wrapper format, fix class hoisting to use var |
| `src/bun.js/api/JSTranspiler.zig` | Add object literal pre-check |
| `src/ast/js_printer.zig` | May need to check object literal printing |
| `test/js/bun/transpiler/repl-transform.test.ts` | Update tests for exact Node.js parity |

---

## Verification

1. Run Node.js preprocess test cases through Bun's transpiler
2. Verify output matches Node.js exactly (or functionally equivalent)
3. Test with vm.runInContext for variable persistence
4. Test object literal inputs: `{a: 1}`, `{foo: await bar()}`

---

## DEPRECATED - Previous Implementation (Already Done)

### 1. Add `replMode` to Bun.Transpiler API

**File**: `src/bun.js/api/JSTranspiler.zig`

Add to `Config` struct (around line 27-44):
```zig
pub const Config = struct {
    // ... existing fields ...
    repl_mode: bool = false,
```

Parse the option in `Config.fromJS()` (around line 420-430):
```zig
if (try object.getBooleanLoose(globalThis, "replMode")) |flag| {
    this.repl_mode = flag;
}
```

Apply the option in `constructor()` (around line 714-721):
```zig
transpiler.options.repl_mode = config.repl_mode;
```

### 2. Add Feature Flag to Runtime

**File**: `src/runtime.zig` (in `Runtime.Features`)

```zig
/// REPL mode: transforms code for interactive evaluation
/// - Wraps lone object literals `{...}` in parentheses
/// - Hoists variable declarations for REPL persistence
/// - Wraps last expression in { value: expr } for result capture
/// - Assigns functions to context for persistence
repl_mode: bool = false,
```

### 3. Add to BundleOptions

**File**: `src/options.zig`

Add to `BundleOptions` struct:
```zig
repl_mode: bool = false,
```

### 4. Implement REPL Transforms in Parser

**File**: `src/ast/P.zig`

#### 4a. Object Literal Detection (Parser-Level)

In REPL mode, the parser should prefer interpreting ambiguous `{...}` as object literals instead of blocks.

**Location**: `src/ast/parseStmt.zig` in statement parsing

When `repl_mode` is true and the parser sees `{` at the start of a statement:
1. Try parsing as expression statement (object literal) first
2. If that fails, fall back to block statement

This is similar to how JavaScript engines handle REPL input. The parser already has the infrastructure to do this - we just need to change the precedence in REPL mode.

```zig
// In parseStmt when repl_mode is true and we see '{'
if (p.options.features.repl_mode and p.token.tag == .t_open_brace) {
    // Try parsing as expression first
    const saved_state = p.saveState();
    if (p.tryParseExpressionStatement()) |expr_stmt| {
        return expr_stmt;
    }
    p.restoreState(saved_state);
    // Fall back to block statement
    return p.parseBlockStatement();
}
```

This handles:
- `{a: 1}` → parsed as object literal expression
- `{a: 1, b: 2}` → parsed as object literal expression
- `{ let x = 1; }` → fails as expression, parsed as block
- `{ label: break label; }` → fails as expression (break not valid in object), parsed as block

#### 4b. REPL Transform Pass (in toAST after visiting)

Add a new function `applyReplTransforms()` that:

1. **Detect if transform is needed**: Walk AST to check for top-level `await`
2. **Skip transform when**:
   - No `await` at top level
   - Top-level `return` statement exists
3. **When transform IS needed**:
   - Wrap entire code in `(async () => { ... })()`
   - Hoist variable declarations outside the async wrapper
   - Convert `const` to `let` for persistence
   - Wrap last expression in `return { value: (expr) }`
   - Handle function declarations (assign to `this`)
   - Handle class declarations (hoist as `let`)

**Key Logic:**
```zig
fn applyReplTransforms(p: *Parser, stmts: []Stmt) ![]Stmt {
    // 1. Check for top-level await
    const has_await = p.hasTopLevelAwait(stmts);
    const has_return = p.hasTopLevelReturn(stmts);

    if (!has_await or has_return) {
        // Just wrap last expression, no async wrapper needed
        return p.wrapLastExpression(stmts);
    }

    // 2. Collect declarations to hoist
    var hoisted = std.ArrayList(Stmt).init(p.allocator);
    var inner_stmts = std.ArrayList(Stmt).init(p.allocator);

    for (stmts) |stmt| {
        switch (stmt.data) {
            .s_local => |local| {
                // Hoist declaration, convert const→let
                try hoisted.append(p.createHoistedDecl(local));
                // Add assignment expression to inner
                try inner_stmts.append(p.createAssignmentExpr(local));
            },
            .s_function => |func| {
                // var foo; (hoisted)
                try hoisted.append(p.createVarDecl(func.name));
                // this.foo = foo; function foo() {} (inner)
                try inner_stmts.append(p.createThisAssignment(func.name));
                try inner_stmts.append(stmt);
            },
            .s_class => |class| {
                // let Foo; (hoisted)
                try hoisted.append(p.createLetDecl(class.name));
                // Foo = class Foo {} (inner)
                try inner_stmts.append(p.createClassAssignment(class));
            },
            else => try inner_stmts.append(stmt),
        }
    }

    // 3. Wrap last expression in return { value: expr }
    p.wrapLastExpressionWithReturn(&inner_stmts);

    // 4. Create async IIFE: (async () => { ...inner... })()
    const async_iife = p.createAsyncIIFE(inner_stmts.items);

    // 5. Combine: hoisted declarations + async IIFE
    try hoisted.append(async_iife);
    return hoisted.toOwnedSlice();
}
```

### 5. TypeScript Type Definitions

**File**: `packages/bun-types/bun.d.ts`

Add to `TranspilerOptions` interface (around line 1748):
```typescript
interface TranspilerOptions {
  // ... existing options ...

  /**
   * Enable REPL mode transforms:
   * - Wraps object literals in parentheses
   * - Hoists declarations for REPL persistence
   * - Wraps last expression in { value: expr } for result capture
   * - Wraps code with await in async IIFE
   */
  replMode?: boolean;
}
```

---

## Files to Modify

| File | Changes |
|------|---------|
| `src/bun.js/api/JSTranspiler.zig` | Add `repl_mode` to Config, parse from JS, apply to transpiler |
| `src/runtime.zig` | Add `repl_mode: bool` to `Runtime.Features` |
| `src/options.zig` | Add `repl_mode: bool` to `BundleOptions` |
| `src/ast/P.zig` | REPL transform pass in `toAST()` |
| `src/ast/parseStmt.zig` | Object literal vs block disambiguation in REPL mode |
| `packages/bun-types/bun.d.ts` | Add `replMode?: boolean` to `TranspilerOptions` |

---

## Test Cases

Create test file: `test/js/bun/transpiler/repl-transform.test.ts`

### Part 1: Transform Output Tests (Unit Tests)

Test exact transformation output matches expected patterns:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("Bun.Transpiler replMode - Transform Output", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  // Based on Node.js test-repl-preprocess-top-level-await.js
  const testCases: [string, string | null][] = [
    // No await = null (no async transform, but still expression capture)
    ['0', null],

    // Basic await
    ['await 0', '(async () => { return { value: (await 0) } })()'],
    ['await 0;', '(async () => { return { value: (await 0) }; })()'],
    ['(await 0)', '(async () => { return ({ value: (await 0) }) })()'],

    // No transform for await inside async functions
    ['async function foo() { await 0; }', null],
    ['async () => await 0', null],
    ['class A { async method() { await 0 } }', null],

    // Top-level return = no transform
    ['await 0; return 0;', null],

    // Multiple await - last one gets return wrapper
    ['await 1; await 2;', '(async () => { await 1; return { value: (await 2) }; })()'],

    // Variable hoisting - var
    ['var a = await 1', 'var a; (async () => { void (a = await 1) })()'],

    // Variable hoisting - let
    ['let a = await 1', 'let a; (async () => { void (a = await 1) })()'],

    // Variable hoisting - const becomes let
    ['const a = await 1', 'let a; (async () => { void (a = await 1) })()'],

    // For loop with var - hoist var
    ['for (var i = 0; i < 1; ++i) { await i }',
     'var i; (async () => { for (void (i = 0); i < 1; ++i) { await i } })()'],

    // For loop with let - no hoist
    ['for (let i = 0; i < 1; ++i) { await i }',
     '(async () => { for (let i = 0; i < 1; ++i) { await i } })()'],

    // Destructuring with var
    ['var {a} = {a:1}, [b] = [1], {c:{d}} = {c:{d: await 1}}',
     'var a, b, d; (async () => { void ( ({a} = {a:1}), ([b] = [1]), ({c:{d}} = {c:{d: await 1}})) })()'],

    // Destructuring with let
    ['let [a, b, c] = await ([1, 2, 3])',
     'let a, b, c; (async () => { void ([a, b, c] = await ([1, 2, 3])) })()'],

    // Function declarations - assign to this
    ['await 0; function foo() {}',
     'var foo; (async () => { await 0; this.foo = foo; function foo() {} })()'],

    // Class declarations - hoist as let
    ['await 0; class Foo {}',
     'let Foo; (async () => { await 0; Foo=class Foo {} })()'],

    // Nested scopes
    ['if (await true) { var a = 1; }',
     'var a; (async () => { if (await true) { void (a = 1); } })()'],
    ['if (await true) { let a = 1; }',
     '(async () => { if (await true) { let a = 1; } })()'],

    // Mixed declarations
    ['var a = await 1; let b = 2; const c = 3;',
     'var a; let b; let c; (async () => { void (a = await 1); void (b = 2); void (c = 3); })()'],

    // for await
    ['for await (var i of asyncIterable) { i; }',
     'var i; (async () => { for await (i of asyncIterable) { i; } })()'],

    // for-of with var
    ['for (var i of [1,2,3]) { await 1; }',
     'var i; (async () => { for (i of [1,2,3]) { await 1; } })()'],

    // for-in with var
    ['for (var i in {x:1}) { await 1 }',
     'var i; (async () => { for (i in {x:1}) { await 1 } })()'],

    // Spread in destructuring
    ['var { ...rest } = await {}',
     'var rest; (async () => { void ({ ...rest } = await {}) })()'],
  ];

  for (const [input, expected] of testCases) {
    test(`transform: ${input.slice(0, 40)}...`, () => {
      const result = transpiler.transformSync(input);
      if (expected === null) {
        // No async transform expected, but expression capture may still happen
        expect(result).not.toMatch(/^\(async/);
      } else {
        expect(result.trim()).toBe(expected);
      }
    });
  }

  // Object literal detection - parser handles this automatically in REPL mode
  describe("object literal vs block disambiguation", () => {
    test("{a: 1} parsed as object literal", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{a: 1}");
      const result = await vm.runInContext(code, ctx);
      // Should evaluate to object, not undefined (block with label)
      expect(result.value).toEqual({ a: 1 });
    });

    test("{a: 1, b: 2} parsed as object literal", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{a: 1, b: 2}");
      const result = await vm.runInContext(code, ctx);
      expect(result.value).toEqual({ a: 1, b: 2 });
    });

    test("{ let x = 1; x } parsed as block", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{ let x = 1; x }");
      const result = await vm.runInContext(code, ctx);
      // Block returns last expression value
      expect(result.value).toBe(1);
    });

    test("{ x: 1; y: 2 } parsed as block with labels", async () => {
      // Semicolons make this a block with labeled statements, not object
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{ x: 1; y: 2 }");
      const result = await vm.runInContext(code, ctx);
      // Block with labels returns last value
      expect(result.value).toBe(2);
    });
  });
});
```

### Part 2: Variable Persistence Tests (Integration with node:vm)

Test that variables persist across multiple REPL evaluations:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL Variable Persistence", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  // Helper to run multiple REPL lines in sequence
  async function runReplSession(lines: string[], context?: object) {
    const ctx = vm.createContext(context ?? { console });
    const results: any[] = [];

    for (const line of lines) {
      const transformed = transpiler.transformSync(line);
      const result = await vm.runInContext(transformed, ctx);
      results.push(result?.value ?? result);
    }

    return { results, context: ctx };
  }

  test("var persists across lines", async () => {
    const { results, context } = await runReplSession([
      "var x = 10",
      "x + 5",
      "x = 20",
      "x",
    ]);

    expect(results[1]).toBe(15);  // x + 5
    expect(results[3]).toBe(20);  // x after reassignment
    expect(context.x).toBe(20);   // x visible in context
  });

  test("let persists across lines (hoisted)", async () => {
    const { results } = await runReplSession([
      "let y = await Promise.resolve(100)",
      "y * 2",
    ]);

    expect(results[1]).toBe(200);
  });

  test("const becomes let, can be reassigned in later lines", async () => {
    const { results } = await runReplSession([
      "const z = await Promise.resolve(5)",
      "z",
      // In REPL, const becomes let, so next line can redeclare
      "z = 10",  // This works because const→let
      "z",
    ]);

    expect(results[1]).toBe(5);
    expect(results[3]).toBe(10);
  });

  test("function declarations persist", async () => {
    const { results, context } = await runReplSession([
      "await 1; function add(a, b) { return a + b; }",
      "add(2, 3)",
      "function multiply(a, b) { return a * b; }",  // no await
      "multiply(4, 5)",
    ]);

    expect(results[1]).toBe(5);
    expect(results[3]).toBe(20);
    expect(typeof context.add).toBe("function");
    expect(typeof context.multiply).toBe("function");
  });

  test("class declarations persist", async () => {
    const { results, context } = await runReplSession([
      "await 1; class Counter { constructor() { this.count = 0; } inc() { this.count++; } }",
      "const c = new Counter()",
      "c.inc(); c.inc(); c.count",
    ]);

    expect(results[2]).toBe(2);
    expect(typeof context.Counter).toBe("function");
  });

  test("complex session with mixed declarations", async () => {
    const { results } = await runReplSession([
      "var total = 0",
      "async function addAsync(n) { return total += await Promise.resolve(n); }",
      "await addAsync(10)",
      "await addAsync(20)",
      "total",
    ]);

    expect(results[2]).toBe(10);
    expect(results[3]).toBe(30);
    expect(results[4]).toBe(30);
  });

  test("destructuring assignment persists", async () => {
    const { results, context } = await runReplSession([
      "var { a, b } = await Promise.resolve({ a: 1, b: 2 })",
      "a + b",
      "var [x, y, z] = [10, 20, 30]",
      "x + y + z",
    ]);

    expect(results[1]).toBe(3);
    expect(results[3]).toBe(60);
    expect(context.a).toBe(1);
    expect(context.x).toBe(10);
  });
});
```

### Part 3: eval() Scoping Semantics Tests

Test that REPL behaves like eval() with proper scoping:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL eval() Scoping Semantics", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  test("var hoists to global context", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("var globalVar = 42");
    await vm.runInContext(code, ctx);

    expect(ctx.globalVar).toBe(42);
  });

  test("let/const hoisted for REPL but scoped correctly", async () => {
    const ctx = vm.createContext({});

    // With await, let is hoisted outside async wrapper
    const code1 = transpiler.transformSync("let x = await 1");
    await vm.runInContext(code1, ctx);
    expect(ctx.x).toBe(1);

    // Without await, let behavior depends on implementation
    const code2 = transpiler.transformSync("let y = 2");
    await vm.runInContext(code2, ctx);
    // y should still be accessible in REPL context
  });

  test("block-scoped let does NOT leak", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("if (await true) { let blockScoped = 1; }");
    await vm.runInContext(code, ctx);

    // blockScoped should NOT be visible in context
    expect(ctx.blockScoped).toBeUndefined();
  });

  test("function in block hoists with var (sloppy mode)", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("if (await true) { function blockFn() { return 42; } }");
    await vm.runInContext(code, ctx);

    // In sloppy mode, function in block hoists to function scope
    expect(typeof ctx.blockFn).toBe("function");
    expect(ctx.blockFn()).toBe(42);
  });

  test("this binding in function declarations", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("await 1; function greet() { return 'hello'; }");
    await vm.runInContext(code, ctx);

    // Function should be assigned to this (context) for REPL persistence
    expect(ctx.greet()).toBe("hello");
  });

  test("async function expression captures result", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("await (async () => { return 42; })()");
    const result = await vm.runInContext(code, ctx);

    expect(result.value).toBe(42);
  });

  test("Promise result NOT auto-awaited due to { value: } wrapper", async () => {
    const ctx = vm.createContext({});

    // Without wrapper, result would be auto-awaited
    // With { value: } wrapper, we get the Promise object
    const code = transpiler.transformSync("await Promise.resolve(Promise.resolve(42))");
    const result = await vm.runInContext(code, ctx);

    // The inner Promise should be in value, not auto-resolved
    expect(result.value).toBeInstanceOf(Promise);
    expect(await result.value).toBe(42);
  });
});
```

### Part 4: Edge Cases and Error Handling

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL Edge Cases", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  test("empty input", () => {
    const result = transpiler.transformSync("");
    expect(result).toBe("");
  });

  test("whitespace only", () => {
    const result = transpiler.transformSync("   \n\t  ");
    expect(result.trim()).toBe("");
  });

  test("comment only", () => {
    const result = transpiler.transformSync("// just a comment");
    expect(result).toContain("// just a comment");
  });

  test("multiline input", () => {
    const input = `
      var x = await 1;
      var y = await 2;
      x + y
    `;
    const result = transpiler.transformSync(input);
    expect(result).toContain("var x");
    expect(result).toContain("var y");
    expect(result).toContain("async");
  });

  test("TypeScript syntax", () => {
    const input = "const x: number = await Promise.resolve(42)";
    const result = transpiler.transformSync(input);
    expect(result).not.toContain(": number"); // Types stripped
    expect(result).toContain("let x");
  });

  test("JSX in REPL", () => {
    const input = "await Promise.resolve(<div>Hello</div>)";
    const result = transpiler.transformSync(input);
    expect(result).toContain("async");
  });

  test("import expression (dynamic)", () => {
    // Dynamic imports should work fine
    const input = "await import('fs')";
    const result = transpiler.transformSync(input);
    expect(result).toContain("import");
  });

  test("nested await expressions", () => {
    const input = "await (await Promise.resolve(Promise.resolve(1)))";
    const result = transpiler.transformSync(input);
    expect(result).toContain("{ value:");
  });

  test("for-await-of", () => {
    const input = `
      async function* gen() { yield 1; yield 2; }
      for await (const x of gen()) { console.log(x); }
    `;
    const result = transpiler.transformSync(input);
    expect(result).toContain("var gen");
    expect(result).toContain("for await");
  });
});
```

---

## Verification Plan

### 1. Build and Basic Tests
```bash
# Build Bun with changes
bun bd

# Run the REPL transform tests
bun bd test test/js/bun/transpiler/repl-transform.test.ts
```

### 2. Manual Transform Output Verification
```typescript
// test-repl-manual.ts
const t = new Bun.Transpiler({ loader: "tsx", replMode: true });

// Object literal
console.log("Object literal:");
console.log(t.transformSync("{a: 1}"));
// Expected: contains "({a: 1})"

// Basic await
console.log("\nBasic await:");
console.log(t.transformSync("await 0"));
// Expected: (async () => { return { value: (await 0) } })()

// Variable hoisting
console.log("\nVar hoisting:");
console.log(t.transformSync("var x = await 1"));
// Expected: var x; (async () => { void (x = await 1) })()

// const → let
console.log("\nConst to let:");
console.log(t.transformSync("const x = await 1"));
// Expected: let x; (async () => { void (x = await 1) })()

// Function hoisting
console.log("\nFunction:");
console.log(t.transformSync("await 0; function foo() {}"));
// Expected: var foo; (async () => { await 0; this.foo = foo; function foo() {} })()
```

### 3. Full REPL Session Simulation
```typescript
// test-repl-session.ts
import vm from "node:vm";

const t = new Bun.Transpiler({ loader: "tsx", replMode: true });
const ctx = vm.createContext({ console, Promise });

async function repl(code: string) {
  const transformed = t.transformSync(code);
  console.log(`> ${code}`);
  console.log(`[transformed]: ${transformed}`);
  const result = await vm.runInContext(transformed, ctx);
  console.log(`= ${JSON.stringify(result?.value ?? result)}\n`);
  return result?.value ?? result;
}

// Test session
await repl("var counter = 0");
await repl("function increment() { return ++counter; }");
await repl("increment()");  // Should be 1
await repl("increment()");  // Should be 2
await repl("counter");      // Should be 2

await repl("const data = await Promise.resolve({ x: 10, y: 20 })");
await repl("data.x + data.y");  // Should be 30

await repl("class Point { constructor(x, y) { this.x = x; this.y = y; } }");
await repl("const p = new Point(3, 4)");
await repl("Math.sqrt(p.x**2 + p.y**2)");  // Should be 5
```

### 4. Verify No Regressions
```bash
# Run existing transpiler tests
bun bd test test/js/bun/transpiler/

# Run existing vm tests
bun bd test test/js/node/vm/
```

### 5. Cross-check with Node.js (Optional)
Compare transform outputs with Node.js's `processTopLevelAwait`:
```typescript
// Compare a few key transforms with Node.js output
const cases = [
  "await 0",
  "var x = await 1",
  "await 0; function foo() {}",
];
// Verify Bun output matches Node.js patterns
```
</claude-plan>
2026-01-19 00:35:49 -08:00
Jarred Sumner
84ef7598fd fix: hoist all declarations as var for context property persistence
In REPL mode, var declarations at the top level become properties of
the vm context object, while let/const do not. Change all hoisting to
use var so that all variables are accessible via context.varName.

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# REPL Transform Fixes and Node.js Parity

## Current Status

The basic `replMode` option is implemented. This plan covers fixes and parity with Node.js REPL.

## Issues to Fix

### 1. Value Wrapper Has Extra Parentheses (CRITICAL)
**Current output:**
```js
({
  __proto__: null,
  value: 42
});
```

**Expected behavior (per Node.js):**
- For **non-async expressions**: Node.js returns `null` (no transform) - the REPL evaluates the expression directly
- For **async expressions**: `(async () => { return { __proto__: null, value: (expr) } })()`

**Solution:**
1. For non-async expressions: Don't wrap in `{ value: expr }` - just return the expression as-is
2. For async expressions: The `{ __proto__: null, value: expr }` is already inside the function after `return`, so no outer parens needed
3. Add inner parens around the expression value for clarity: `{ __proto__: null, value: (expr) }`

### 2. Object Literal Disambiguation (CRITICAL)
**Input:** `{a: 1}` or `{foo: await fetch()}`

**Current:** Parsed as block with labeled statement, NOT object literal

**Solution:** Pre-check input at transpiler layer:
- If code starts with `{` and doesn't end with `;`, try parsing as `(_=CODE)`
- If valid, wrap input as `(CODE)` before processing
- This matches Node.js approach in `repl.js` line 411-414

### 3. Class Declarations Don't Persist to VM Context
**Current:** Uses `let ClassName;` hoisting - doesn't become vm context property

**Node.js behavior:** Also uses `let` - this is a known limitation in Node.js too!

Looking at Node.js `await.js` line 31-37:
```js
ClassDeclaration(node, state, c) {
  state.prepend(node, `${node.id.name}=`);
  ArrayPrototypePush(state.hoistedDeclarationStatements, `let ${node.id.name}; `);
}
```

**Decision:** Use `var` instead of `let` for class hoisting. This makes classes persist to vm context, matching user expectations for REPL behavior. (Different from Node.js which uses `let`)

---

## Implementation Plan

## Usage Example

```typescript
// REPL tool implementation
const transpiler = new Bun.Transpiler({
  loader: "tsx",
  replMode: true,  // NEW OPTION
});

// For each REPL input line:
const transformed = transpiler.transformSync(userInput);

// Execute in persistent VM context
const result = vm.runInContext(transformed, replContext);

// result.value contains the expression result (wrapped to prevent auto-await)
console.log(result.value);
```

## Design Decisions

- **Value wrapper**: Use `{ value: expr }` wrapper like Node.js to prevent auto-awaiting Promise results
- **Static imports**: Keep static imports as-is (Bun handles them natively)
- **Scope**: Full Node.js REPL transform parity

---

## Node.js REPL Transform Behavior (Reference)

From `vendor/node/lib/internal/repl/await.js`:

### 1. Object Literal Detection
```javascript
// {a:1} → ({a:1}) when starts with { and no trailing ;
if (/^\s*{/.test(code) && !/;\s*$/.test(code)) {
  code = `(${code})`;
}
```

### 2. Top-Level Await Transform
```javascript
// Input:  await x
// Output: (async () => { return { value: (await x) } })()

// Input:  var x = await 1
// Output: var x; (async () => { void (x = await 1) })()

// Input:  const x = await 1
// Output: let x; (async () => { void (x = await 1) })()  // const→let

// Input:  function foo() {}  (with await somewhere)
// Output: var foo; (async () => { this.foo = foo; function foo() {} })()

// Input:  class Foo {}  (with await somewhere)
// Output: let Foo; (async () => { Foo=class Foo {} })()
```

### 3. Transform Skipping
Returns `null` (no transform) when:
- No `await` expression present at top level
- Top-level `return` statement exists
- Code is inside async functions/arrow functions/class methods

---

## Implementation Plan

### Fix 1: Remove Extra Parentheses from Value Wrapper

**Problem:** The printer adds `()` around objects at statement start to disambiguate from blocks.

**Solution:** Always use an IIFE wrapper (sync or async) so the object is after `return`:

```js
// Non-async expression (current - BAD)
({ __proto__: null, value: 42 });

// Non-async expression (fixed - GOOD)
(() => { return { __proto__: null, value: 42 } })()

// Non-async with hoisting (fixed - GOOD)
var x;
(() => { void (x = 1); return { __proto__: null, value: x } })()

// Async (already correct)
var x;
(async () => { void (x = await 1); return { __proto__: null, value: x } })()
```

**Files to modify:**
1. `src/ast/P.zig` - `applyReplValueWrapper()` function

**Changes:**
- Remove the simple `{ value: expr }` wrapper approach
- Always use `applyReplAsyncTransform()` style IIFE wrapping, but with `is_async = false` for non-async code
- This ensures the object is always after `return`, avoiding the parentheses issue
- Hoisting still works for both cases

### Fix 2: Object Literal Disambiguation

**File:** `src/bun.js/api/JSTranspiler.zig` - Before parsing

Add pre-processing check:
```zig
// In transformSync, before parsing:
if (config.repl_mode) {
    // Check if input looks like object literal: starts with { and doesn't end with ;
    if (startsWithBrace(source) and !endsWithSemicolon(source)) {
        // Try parsing as expression by wrapping: _=(CODE)
        // If valid, wrap input as (CODE)
        source = wrapAsExpression(source);
    }
}
```

This matches Node.js `isObjectLiteral()` check in `repl/utils.js:786-789`:
```js
function isObjectLiteral(code) {
  return /^\s*{/.test(code) && !/;\s*$/.test(code);
}
```

### Fix 3: Class Declaration Persistence

**File:** `src/ast/P.zig` - `applyReplAsyncTransform()` in the class handling section

Change from:
```zig
// let Foo; (hoisted)
try hoisted_stmts.append(p.s(S.Local{ .kind = .k_let, ... }));
```

To:
```zig
// var Foo; (hoisted) - use var so it becomes context property
try hoisted_stmts.append(p.s(S.Local{ .kind = .k_var, ... }));
// Also add: this.Foo = Foo; assignment after class declaration
```

---

## Node.js REPL Test Cases to Match

From `vendor/node/test/parallel/test-repl-preprocess-top-level-await.js`:

| Input | Expected Output |
|-------|-----------------|
| `await 0` | `(async () => { return { value: (await 0) } })()` |
| `var a = await 1` | `var a; (async () => { void (a = await 1) })()` |
| `let a = await 1` | `let a; (async () => { void (a = await 1) })()` |
| `const a = await 1` | `let a; (async () => { void (a = await 1) })()` |
| `await 0; function foo() {}` | `var foo; (async () => { await 0; this.foo = foo; function foo() {} })()` |
| `await 0; class Foo {}` | `let Foo; (async () => { await 0; Foo=class Foo {} })()` |
| `var {a} = {a:1}, [b] = [1]` | `var a, b; (async () => { void ( ({a} = {a:1}), ([b] = [1])) })()` |

---

## Files to Modify

| File | Changes |
|------|---------|
| `src/ast/P.zig` | Fix value wrapper format, fix class hoisting to use var |
| `src/bun.js/api/JSTranspiler.zig` | Add object literal pre-check |
| `src/ast/js_printer.zig` | May need to check object literal printing |
| `test/js/bun/transpiler/repl-transform.test.ts` | Update tests for exact Node.js parity |

---

## Verification

1. Run Node.js preprocess test cases through Bun's transpiler
2. Verify output matches Node.js exactly (or functionally equivalent)
3. Test with vm.runInContext for variable persistence
4. Test object literal inputs: `{a: 1}`, `{foo: await bar()}`

---

## DEPRECATED - Previous Implementation (Already Done)

### 1. Add `replMode` to Bun.Transpiler API

**File**: `src/bun.js/api/JSTranspiler.zig`

Add to `Config` struct (around line 27-44):
```zig
pub const Config = struct {
    // ... existing fields ...
    repl_mode: bool = false,
```

Parse the option in `Config.fromJS()` (around line 420-430):
```zig
if (try object.getBooleanLoose(globalThis, "replMode")) |flag| {
    this.repl_mode = flag;
}
```

Apply the option in `constructor()` (around line 714-721):
```zig
transpiler.options.repl_mode = config.repl_mode;
```

### 2. Add Feature Flag to Runtime

**File**: `src/runtime.zig` (in `Runtime.Features`)

```zig
/// REPL mode: transforms code for interactive evaluation
/// - Wraps lone object literals `{...}` in parentheses
/// - Hoists variable declarations for REPL persistence
/// - Wraps last expression in { value: expr } for result capture
/// - Assigns functions to context for persistence
repl_mode: bool = false,
```

### 3. Add to BundleOptions

**File**: `src/options.zig`

Add to `BundleOptions` struct:
```zig
repl_mode: bool = false,
```

### 4. Implement REPL Transforms in Parser

**File**: `src/ast/P.zig`

#### 4a. Object Literal Detection (Parser-Level)

In REPL mode, the parser should prefer interpreting ambiguous `{...}` as object literals instead of blocks.

**Location**: `src/ast/parseStmt.zig` in statement parsing

When `repl_mode` is true and the parser sees `{` at the start of a statement:
1. Try parsing as expression statement (object literal) first
2. If that fails, fall back to block statement

This is similar to how JavaScript engines handle REPL input. The parser already has the infrastructure to do this - we just need to change the precedence in REPL mode.

```zig
// In parseStmt when repl_mode is true and we see '{'
if (p.options.features.repl_mode and p.token.tag == .t_open_brace) {
    // Try parsing as expression first
    const saved_state = p.saveState();
    if (p.tryParseExpressionStatement()) |expr_stmt| {
        return expr_stmt;
    }
    p.restoreState(saved_state);
    // Fall back to block statement
    return p.parseBlockStatement();
}
```

This handles:
- `{a: 1}` → parsed as object literal expression
- `{a: 1, b: 2}` → parsed as object literal expression
- `{ let x = 1; }` → fails as expression, parsed as block
- `{ label: break label; }` → fails as expression (break not valid in object), parsed as block

#### 4b. REPL Transform Pass (in toAST after visiting)

Add a new function `applyReplTransforms()` that:

1. **Detect if transform is needed**: Walk AST to check for top-level `await`
2. **Skip transform when**:
   - No `await` at top level
   - Top-level `return` statement exists
3. **When transform IS needed**:
   - Wrap entire code in `(async () => { ... })()`
   - Hoist variable declarations outside the async wrapper
   - Convert `const` to `let` for persistence
   - Wrap last expression in `return { value: (expr) }`
   - Handle function declarations (assign to `this`)
   - Handle class declarations (hoist as `let`)

**Key Logic:**
```zig
fn applyReplTransforms(p: *Parser, stmts: []Stmt) ![]Stmt {
    // 1. Check for top-level await
    const has_await = p.hasTopLevelAwait(stmts);
    const has_return = p.hasTopLevelReturn(stmts);

    if (!has_await or has_return) {
        // Just wrap last expression, no async wrapper needed
        return p.wrapLastExpression(stmts);
    }

    // 2. Collect declarations to hoist
    var hoisted = std.ArrayList(Stmt).init(p.allocator);
    var inner_stmts = std.ArrayList(Stmt).init(p.allocator);

    for (stmts) |stmt| {
        switch (stmt.data) {
            .s_local => |local| {
                // Hoist declaration, convert const→let
                try hoisted.append(p.createHoistedDecl(local));
                // Add assignment expression to inner
                try inner_stmts.append(p.createAssignmentExpr(local));
            },
            .s_function => |func| {
                // var foo; (hoisted)
                try hoisted.append(p.createVarDecl(func.name));
                // this.foo = foo; function foo() {} (inner)
                try inner_stmts.append(p.createThisAssignment(func.name));
                try inner_stmts.append(stmt);
            },
            .s_class => |class| {
                // let Foo; (hoisted)
                try hoisted.append(p.createLetDecl(class.name));
                // Foo = class Foo {} (inner)
                try inner_stmts.append(p.createClassAssignment(class));
            },
            else => try inner_stmts.append(stmt),
        }
    }

    // 3. Wrap last expression in return { value: expr }
    p.wrapLastExpressionWithReturn(&inner_stmts);

    // 4. Create async IIFE: (async () => { ...inner... })()
    const async_iife = p.createAsyncIIFE(inner_stmts.items);

    // 5. Combine: hoisted declarations + async IIFE
    try hoisted.append(async_iife);
    return hoisted.toOwnedSlice();
}
```

### 5. TypeScript Type Definitions

**File**: `packages/bun-types/bun.d.ts`

Add to `TranspilerOptions` interface (around line 1748):
```typescript
interface TranspilerOptions {
  // ... existing options ...

  /**
   * Enable REPL mode transforms:
   * - Wraps object literals in parentheses
   * - Hoists declarations for REPL persistence
   * - Wraps last expression in { value: expr } for result capture
   * - Wraps code with await in async IIFE
   */
  replMode?: boolean;
}
```

---

## Files to Modify

| File | Changes |
|------|---------|
| `src/bun.js/api/JSTranspiler.zig` | Add `repl_mode` to Config, parse from JS, apply to transpiler |
| `src/runtime.zig` | Add `repl_mode: bool` to `Runtime.Features` |
| `src/options.zig` | Add `repl_mode: bool` to `BundleOptions` |
| `src/ast/P.zig` | REPL transform pass in `toAST()` |
| `src/ast/parseStmt.zig` | Object literal vs block disambiguation in REPL mode |
| `packages/bun-types/bun.d.ts` | Add `replMode?: boolean` to `TranspilerOptions` |

---

## Test Cases

Create test file: `test/js/bun/transpiler/repl-transform.test.ts`

### Part 1: Transform Output Tests (Unit Tests)

Test exact transformation output matches expected patterns:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("Bun.Transpiler replMode - Transform Output", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  // Based on Node.js test-repl-preprocess-top-level-await.js
  const testCases: [string, string | null][] = [
    // No await = null (no async transform, but still expression capture)
    ['0', null],

    // Basic await
    ['await 0', '(async () => { return { value: (await 0) } })()'],
    ['await 0;', '(async () => { return { value: (await 0) }; })()'],
    ['(await 0)', '(async () => { return ({ value: (await 0) }) })()'],

    // No transform for await inside async functions
    ['async function foo() { await 0; }', null],
    ['async () => await 0', null],
    ['class A { async method() { await 0 } }', null],

    // Top-level return = no transform
    ['await 0; return 0;', null],

    // Multiple await - last one gets return wrapper
    ['await 1; await 2;', '(async () => { await 1; return { value: (await 2) }; })()'],

    // Variable hoisting - var
    ['var a = await 1', 'var a; (async () => { void (a = await 1) })()'],

    // Variable hoisting - let
    ['let a = await 1', 'let a; (async () => { void (a = await 1) })()'],

    // Variable hoisting - const becomes let
    ['const a = await 1', 'let a; (async () => { void (a = await 1) })()'],

    // For loop with var - hoist var
    ['for (var i = 0; i < 1; ++i) { await i }',
     'var i; (async () => { for (void (i = 0); i < 1; ++i) { await i } })()'],

    // For loop with let - no hoist
    ['for (let i = 0; i < 1; ++i) { await i }',
     '(async () => { for (let i = 0; i < 1; ++i) { await i } })()'],

    // Destructuring with var
    ['var {a} = {a:1}, [b] = [1], {c:{d}} = {c:{d: await 1}}',
     'var a, b, d; (async () => { void ( ({a} = {a:1}), ([b] = [1]), ({c:{d}} = {c:{d: await 1}})) })()'],

    // Destructuring with let
    ['let [a, b, c] = await ([1, 2, 3])',
     'let a, b, c; (async () => { void ([a, b, c] = await ([1, 2, 3])) })()'],

    // Function declarations - assign to this
    ['await 0; function foo() {}',
     'var foo; (async () => { await 0; this.foo = foo; function foo() {} })()'],

    // Class declarations - hoist as let
    ['await 0; class Foo {}',
     'let Foo; (async () => { await 0; Foo=class Foo {} })()'],

    // Nested scopes
    ['if (await true) { var a = 1; }',
     'var a; (async () => { if (await true) { void (a = 1); } })()'],
    ['if (await true) { let a = 1; }',
     '(async () => { if (await true) { let a = 1; } })()'],

    // Mixed declarations
    ['var a = await 1; let b = 2; const c = 3;',
     'var a; let b; let c; (async () => { void (a = await 1); void (b = 2); void (c = 3); })()'],

    // for await
    ['for await (var i of asyncIterable) { i; }',
     'var i; (async () => { for await (i of asyncIterable) { i; } })()'],

    // for-of with var
    ['for (var i of [1,2,3]) { await 1; }',
     'var i; (async () => { for (i of [1,2,3]) { await 1; } })()'],

    // for-in with var
    ['for (var i in {x:1}) { await 1 }',
     'var i; (async () => { for (i in {x:1}) { await 1 } })()'],

    // Spread in destructuring
    ['var { ...rest } = await {}',
     'var rest; (async () => { void ({ ...rest } = await {}) })()'],
  ];

  for (const [input, expected] of testCases) {
    test(`transform: ${input.slice(0, 40)}...`, () => {
      const result = transpiler.transformSync(input);
      if (expected === null) {
        // No async transform expected, but expression capture may still happen
        expect(result).not.toMatch(/^\(async/);
      } else {
        expect(result.trim()).toBe(expected);
      }
    });
  }

  // Object literal detection - parser handles this automatically in REPL mode
  describe("object literal vs block disambiguation", () => {
    test("{a: 1} parsed as object literal", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{a: 1}");
      const result = await vm.runInContext(code, ctx);
      // Should evaluate to object, not undefined (block with label)
      expect(result.value).toEqual({ a: 1 });
    });

    test("{a: 1, b: 2} parsed as object literal", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{a: 1, b: 2}");
      const result = await vm.runInContext(code, ctx);
      expect(result.value).toEqual({ a: 1, b: 2 });
    });

    test("{ let x = 1; x } parsed as block", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{ let x = 1; x }");
      const result = await vm.runInContext(code, ctx);
      // Block returns last expression value
      expect(result.value).toBe(1);
    });

    test("{ x: 1; y: 2 } parsed as block with labels", async () => {
      // Semicolons make this a block with labeled statements, not object
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{ x: 1; y: 2 }");
      const result = await vm.runInContext(code, ctx);
      // Block with labels returns last value
      expect(result.value).toBe(2);
    });
  });
});
```

### Part 2: Variable Persistence Tests (Integration with node:vm)

Test that variables persist across multiple REPL evaluations:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL Variable Persistence", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  // Helper to run multiple REPL lines in sequence
  async function runReplSession(lines: string[], context?: object) {
    const ctx = vm.createContext(context ?? { console });
    const results: any[] = [];

    for (const line of lines) {
      const transformed = transpiler.transformSync(line);
      const result = await vm.runInContext(transformed, ctx);
      results.push(result?.value ?? result);
    }

    return { results, context: ctx };
  }

  test("var persists across lines", async () => {
    const { results, context } = await runReplSession([
      "var x = 10",
      "x + 5",
      "x = 20",
      "x",
    ]);

    expect(results[1]).toBe(15);  // x + 5
    expect(results[3]).toBe(20);  // x after reassignment
    expect(context.x).toBe(20);   // x visible in context
  });

  test("let persists across lines (hoisted)", async () => {
    const { results } = await runReplSession([
      "let y = await Promise.resolve(100)",
      "y * 2",
    ]);

    expect(results[1]).toBe(200);
  });

  test("const becomes let, can be reassigned in later lines", async () => {
    const { results } = await runReplSession([
      "const z = await Promise.resolve(5)",
      "z",
      // In REPL, const becomes let, so next line can redeclare
      "z = 10",  // This works because const→let
      "z",
    ]);

    expect(results[1]).toBe(5);
    expect(results[3]).toBe(10);
  });

  test("function declarations persist", async () => {
    const { results, context } = await runReplSession([
      "await 1; function add(a, b) { return a + b; }",
      "add(2, 3)",
      "function multiply(a, b) { return a * b; }",  // no await
      "multiply(4, 5)",
    ]);

    expect(results[1]).toBe(5);
    expect(results[3]).toBe(20);
    expect(typeof context.add).toBe("function");
    expect(typeof context.multiply).toBe("function");
  });

  test("class declarations persist", async () => {
    const { results, context } = await runReplSession([
      "await 1; class Counter { constructor() { this.count = 0; } inc() { this.count++; } }",
      "const c = new Counter()",
      "c.inc(); c.inc(); c.count",
    ]);

    expect(results[2]).toBe(2);
    expect(typeof context.Counter).toBe("function");
  });

  test("complex session with mixed declarations", async () => {
    const { results } = await runReplSession([
      "var total = 0",
      "async function addAsync(n) { return total += await Promise.resolve(n); }",
      "await addAsync(10)",
      "await addAsync(20)",
      "total",
    ]);

    expect(results[2]).toBe(10);
    expect(results[3]).toBe(30);
    expect(results[4]).toBe(30);
  });

  test("destructuring assignment persists", async () => {
    const { results, context } = await runReplSession([
      "var { a, b } = await Promise.resolve({ a: 1, b: 2 })",
      "a + b",
      "var [x, y, z] = [10, 20, 30]",
      "x + y + z",
    ]);

    expect(results[1]).toBe(3);
    expect(results[3]).toBe(60);
    expect(context.a).toBe(1);
    expect(context.x).toBe(10);
  });
});
```

### Part 3: eval() Scoping Semantics Tests

Test that REPL behaves like eval() with proper scoping:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL eval() Scoping Semantics", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  test("var hoists to global context", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("var globalVar = 42");
    await vm.runInContext(code, ctx);

    expect(ctx.globalVar).toBe(42);
  });

  test("let/const hoisted for REPL but scoped correctly", async () => {
    const ctx = vm.createContext({});

    // With await, let is hoisted outside async wrapper
    const code1 = transpiler.transformSync("let x = await 1");
    await vm.runInContext(code1, ctx);
    expect(ctx.x).toBe(1);

    // Without await, let behavior depends on implementation
    const code2 = transpiler.transformSync("let y = 2");
    await vm.runInContext(code2, ctx);
    // y should still be accessible in REPL context
  });

  test("block-scoped let does NOT leak", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("if (await true) { let blockScoped = 1; }");
    await vm.runInContext(code, ctx);

    // blockScoped should NOT be visible in context
    expect(ctx.blockScoped).toBeUndefined();
  });

  test("function in block hoists with var (sloppy mode)", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("if (await true) { function blockFn() { return 42; } }");
    await vm.runInContext(code, ctx);

    // In sloppy mode, function in block hoists to function scope
    expect(typeof ctx.blockFn).toBe("function");
    expect(ctx.blockFn()).toBe(42);
  });

  test("this binding in function declarations", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("await 1; function greet() { return 'hello'; }");
    await vm.runInContext(code, ctx);

    // Function should be assigned to this (context) for REPL persistence
    expect(ctx.greet()).toBe("hello");
  });

  test("async function expression captures result", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("await (async () => { return 42; })()");
    const result = await vm.runInContext(code, ctx);

    expect(result.value).toBe(42);
  });

  test("Promise result NOT auto-awaited due to { value: } wrapper", async () => {
    const ctx = vm.createContext({});

    // Without wrapper, result would be auto-awaited
    // With { value: } wrapper, we get the Promise object
    const code = transpiler.transformSync("await Promise.resolve(Promise.resolve(42))");
    const result = await vm.runInContext(code, ctx);

    // The inner Promise should be in value, not auto-resolved
    expect(result.value).toBeInstanceOf(Promise);
    expect(await result.value).toBe(42);
  });
});
```

### Part 4: Edge Cases and Error Handling

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL Edge Cases", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  test("empty input", () => {
    const result = transpiler.transformSync("");
    expect(result).toBe("");
  });

  test("whitespace only", () => {
    const result = transpiler.transformSync("   \n\t  ");
    expect(result.trim()).toBe("");
  });

  test("comment only", () => {
    const result = transpiler.transformSync("// just a comment");
    expect(result).toContain("// just a comment");
  });

  test("multiline input", () => {
    const input = `
      var x = await 1;
      var y = await 2;
      x + y
    `;
    const result = transpiler.transformSync(input);
    expect(result).toContain("var x");
    expect(result).toContain("var y");
    expect(result).toContain("async");
  });

  test("TypeScript syntax", () => {
    const input = "const x: number = await Promise.resolve(42)";
    const result = transpiler.transformSync(input);
    expect(result).not.toContain(": number"); // Types stripped
    expect(result).toContain("let x");
  });

  test("JSX in REPL", () => {
    const input = "await Promise.resolve(<div>Hello</div>)";
    const result = transpiler.transformSync(input);
    expect(result).toContain("async");
  });

  test("import expression (dynamic)", () => {
    // Dynamic imports should work fine
    const input = "await import('fs')";
    const result = transpiler.transformSync(input);
    expect(result).toContain("import");
  });

  test("nested await expressions", () => {
    const input = "await (await Promise.resolve(Promise.resolve(1)))";
    const result = transpiler.transformSync(input);
    expect(result).toContain("{ value:");
  });

  test("for-await-of", () => {
    const input = `
      async function* gen() { yield 1; yield 2; }
      for await (const x of gen()) { console.log(x); }
    `;
    const result = transpiler.transformSync(input);
    expect(result).toContain("var gen");
    expect(result).toContain("for await");
  });
});
```

---

## Verification Plan

### 1. Build and Basic Tests
```bash
# Build Bun with changes
bun bd

# Run the REPL transform tests
bun bd test test/js/bun/transpiler/repl-transform.test.ts
```

### 2. Manual Transform Output Verification
```typescript
// test-repl-manual.ts
const t = new Bun.Transpiler({ loader: "tsx", replMode: true });

// Object literal
console.log("Object literal:");
console.log(t.transformSync("{a: 1}"));
// Expected: contains "({a: 1})"

// Basic await
console.log("\nBasic await:");
console.log(t.transformSync("await 0"));
// Expected: (async () => { return { value: (await 0) } })()

// Variable hoisting
console.log("\nVar hoisting:");
console.log(t.transformSync("var x = await 1"));
// Expected: var x; (async () => { void (x = await 1) })()

// const → let
console.log("\nConst to let:");
console.log(t.transformSync("const x = await 1"));
// Expected: let x; (async () => { void (x = await 1) })()

// Function hoisting
console.log("\nFunction:");
console.log(t.transformSync("await 0; function foo() {}"));
// Expected: var foo; (async () => { await 0; this.foo = foo; function foo() {} })()
```

### 3. Full REPL Session Simulation
```typescript
// test-repl-session.ts
import vm from "node:vm";

const t = new Bun.Transpiler({ loader: "tsx", replMode: true });
const ctx = vm.createContext({ console, Promise });

async function repl(code: string) {
  const transformed = t.transformSync(code);
  console.log(`> ${code}`);
  console.log(`[transformed]: ${transformed}`);
  const result = await vm.runInContext(transformed, ctx);
  console.log(`= ${JSON.stringify(result?.value ?? result)}\n`);
  return result?.value ?? result;
}

// Test session
await repl("var counter = 0");
await repl("function increment() { return ++counter; }");
await repl("increment()");  // Should be 1
await repl("increment()");  // Should be 2
await repl("counter");      // Should be 2

await repl("const data = await Promise.resolve({ x: 10, y: 20 })");
await repl("data.x + data.y");  // Should be 30

await repl("class Point { constructor(x, y) { this.x = x; this.y = y; } }");
await repl("const p = new Point(3, 4)");
await repl("Math.sqrt(p.x**2 + p.y**2)");  // Should be 5
```

### 4. Verify No Regressions
```bash
# Run existing transpiler tests
bun bd test test/js/bun/transpiler/

# Run existing vm tests
bun bd test test/js/node/vm/
```

### 5. Cross-check with Node.js (Optional)
Compare transform outputs with Node.js's `processTopLevelAwait`:
```typescript
// Compare a few key transforms with Node.js output
const cases = [
  "await 0",
  "var x = await 1",
  "await 0; function foo() {}",
];
// Verify Bun output matches Node.js patterns
```
</claude-plan>
2026-01-19 00:32:55 -08:00
autofix-ci[bot]
b001c9afc0 [autofix.ci] apply automated fixes 2026-01-19 08:26:29 +00:00
Jarred Sumner
5a0705348b feat(transpiler): add replMode option for REPL transforms
Add a new `replMode` option to Bun.Transpiler that transforms code for
interactive REPL evaluation:

- Wraps expressions in `{ value: expr }` for result capture
- Uses sync/async IIFE wrappers to avoid parentheses around objects
- Hoists var/let/const declarations for persistence across REPL lines
- Converts const to let for REPL mutability
- Hoists function declarations with this.funcName assignment
- Hoists class declarations with var for vm context persistence
- Auto-detects object literals (starting with { without trailing ;)

This enables building a Node.js-compatible REPL using Bun.Transpiler
with vm.runInContext for persistent variable scope.

Usage:
```typescript
const transpiler = new Bun.Transpiler({
  loader: "tsx",
  replMode: true,
});
const transformed = transpiler.transformSync(userInput);
const result = await vm.runInContext(transformed, context);
console.log(result.value);
```

REPL transforms are extracted into separate repl_transforms.zig module.

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 8
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# REPL Transform Fixes and Node.js Parity

## Current Status

The basic `replMode` option is implemented. This plan covers fixes and parity with Node.js REPL.

## Issues to Fix

### 1. Value Wrapper Has Extra Parentheses (CRITICAL)
**Current output:**
```js
({
  __proto__: null,
  value: 42
});
```

**Expected behavior (per Node.js):**
- For **non-async expressions**: Node.js returns `null` (no transform) - the REPL evaluates the expression directly
- For **async expressions**: `(async () => { return { __proto__: null, value: (expr) } })()`

**Solution:**
1. For non-async expressions: Don't wrap in `{ value: expr }` - just return the expression as-is
2. For async expressions: The `{ __proto__: null, value: expr }` is already inside the function after `return`, so no outer parens needed
3. Add inner parens around the expression value for clarity: `{ __proto__: null, value: (expr) }`

### 2. Object Literal Disambiguation (CRITICAL)
**Input:** `{a: 1}` or `{foo: await fetch()}`

**Current:** Parsed as block with labeled statement, NOT object literal

**Solution:** Pre-check input at transpiler layer:
- If code starts with `{` and doesn't end with `;`, try parsing as `(_=CODE)`
- If valid, wrap input as `(CODE)` before processing
- This matches Node.js approach in `repl.js` line 411-414

### 3. Class Declarations Don't Persist to VM Context
**Current:** Uses `let ClassName;` hoisting - doesn't become vm context property

**Node.js behavior:** Also uses `let` - this is a known limitation in Node.js too!

Looking at Node.js `await.js` line 31-37:
```js
ClassDeclaration(node, state, c) {
  state.prepend(node, `${node.id.name}=`);
  ArrayPrototypePush(state.hoistedDeclarationStatements, `let ${node.id.name}; `);
}
```

**Decision:** Use `var` instead of `let` for class hoisting. This makes classes persist to vm context, matching user expectations for REPL behavior. (Different from Node.js which uses `let`)

---

## Implementation Plan

## Usage Example

```typescript
// REPL tool implementation
const transpiler = new Bun.Transpiler({
  loader: "tsx",
  replMode: true,  // NEW OPTION
});

// For each REPL input line:
const transformed = transpiler.transformSync(userInput);

// Execute in persistent VM context
const result = vm.runInContext(transformed, replContext);

// result.value contains the expression result (wrapped to prevent auto-await)
console.log(result.value);
```

## Design Decisions

- **Value wrapper**: Use `{ value: expr }` wrapper like Node.js to prevent auto-awaiting Promise results
- **Static imports**: Keep static imports as-is (Bun handles them natively)
- **Scope**: Full Node.js REPL transform parity

---

## Node.js REPL Transform Behavior (Reference)

From `vendor/node/lib/internal/repl/await.js`:

### 1. Object Literal Detection
```javascript
// {a:1} → ({a:1}) when starts with { and no trailing ;
if (/^\s*{/.test(code) && !/;\s*$/.test(code)) {
  code = `(${code})`;
}
```

### 2. Top-Level Await Transform
```javascript
// Input:  await x
// Output: (async () => { return { value: (await x) } })()

// Input:  var x = await 1
// Output: var x; (async () => { void (x = await 1) })()

// Input:  const x = await 1
// Output: let x; (async () => { void (x = await 1) })()  // const→let

// Input:  function foo() {}  (with await somewhere)
// Output: var foo; (async () => { this.foo = foo; function foo() {} })()

// Input:  class Foo {}  (with await somewhere)
// Output: let Foo; (async () => { Foo=class Foo {} })()
```

### 3. Transform Skipping
Returns `null` (no transform) when:
- No `await` expression present at top level
- Top-level `return` statement exists
- Code is inside async functions/arrow functions/class methods

---

## Implementation Plan

### Fix 1: Remove Extra Parentheses from Value Wrapper

**Problem:** The printer adds `()` around objects at statement start to disambiguate from blocks.

**Solution:** Always use an IIFE wrapper (sync or async) so the object is after `return`:

```js
// Non-async expression (current - BAD)
({ __proto__: null, value: 42 });

// Non-async expression (fixed - GOOD)
(() => { return { __proto__: null, value: 42 } })()

// Non-async with hoisting (fixed - GOOD)
var x;
(() => { void (x = 1); return { __proto__: null, value: x } })()

// Async (already correct)
var x;
(async () => { void (x = await 1); return { __proto__: null, value: x } })()
```

**Files to modify:**
1. `src/ast/P.zig` - `applyReplValueWrapper()` function

**Changes:**
- Remove the simple `{ value: expr }` wrapper approach
- Always use `applyReplAsyncTransform()` style IIFE wrapping, but with `is_async = false` for non-async code
- This ensures the object is always after `return`, avoiding the parentheses issue
- Hoisting still works for both cases

### Fix 2: Object Literal Disambiguation

**File:** `src/bun.js/api/JSTranspiler.zig` - Before parsing

Add pre-processing check:
```zig
// In transformSync, before parsing:
if (config.repl_mode) {
    // Check if input looks like object literal: starts with { and doesn't end with ;
    if (startsWithBrace(source) and !endsWithSemicolon(source)) {
        // Try parsing as expression by wrapping: _=(CODE)
        // If valid, wrap input as (CODE)
        source = wrapAsExpression(source);
    }
}
```

This matches Node.js `isObjectLiteral()` check in `repl/utils.js:786-789`:
```js
function isObjectLiteral(code) {
  return /^\s*{/.test(code) && !/;\s*$/.test(code);
}
```

### Fix 3: Class Declaration Persistence

**File:** `src/ast/P.zig` - `applyReplAsyncTransform()` in the class handling section

Change from:
```zig
// let Foo; (hoisted)
try hoisted_stmts.append(p.s(S.Local{ .kind = .k_let, ... }));
```

To:
```zig
// var Foo; (hoisted) - use var so it becomes context property
try hoisted_stmts.append(p.s(S.Local{ .kind = .k_var, ... }));
// Also add: this.Foo = Foo; assignment after class declaration
```

---

## Node.js REPL Test Cases to Match

From `vendor/node/test/parallel/test-repl-preprocess-top-level-await.js`:

| Input | Expected Output |
|-------|-----------------|
| `await 0` | `(async () => { return { value: (await 0) } })()` |
| `var a = await 1` | `var a; (async () => { void (a = await 1) })()` |
| `let a = await 1` | `let a; (async () => { void (a = await 1) })()` |
| `const a = await 1` | `let a; (async () => { void (a = await 1) })()` |
| `await 0; function foo() {}` | `var foo; (async () => { await 0; this.foo = foo; function foo() {} })()` |
| `await 0; class Foo {}` | `let Foo; (async () => { await 0; Foo=class Foo {} })()` |
| `var {a} = {a:1}, [b] = [1]` | `var a, b; (async () => { void ( ({a} = {a:1}), ([b] = [1])) })()` |

---

## Files to Modify

| File | Changes |
|------|---------|
| `src/ast/P.zig` | Fix value wrapper format, fix class hoisting to use var |
| `src/bun.js/api/JSTranspiler.zig` | Add object literal pre-check |
| `src/ast/js_printer.zig` | May need to check object literal printing |
| `test/js/bun/transpiler/repl-transform.test.ts` | Update tests for exact Node.js parity |

---

## Verification

1. Run Node.js preprocess test cases through Bun's transpiler
2. Verify output matches Node.js exactly (or functionally equivalent)
3. Test with vm.runInContext for variable persistence
4. Test object literal inputs: `{a: 1}`, `{foo: await bar()}`

---

## DEPRECATED - Previous Implementation (Already Done)

### 1. Add `replMode` to Bun.Transpiler API

**File**: `src/bun.js/api/JSTranspiler.zig`

Add to `Config` struct (around line 27-44):
```zig
pub const Config = struct {
    // ... existing fields ...
    repl_mode: bool = false,
```

Parse the option in `Config.fromJS()` (around line 420-430):
```zig
if (try object.getBooleanLoose(globalThis, "replMode")) |flag| {
    this.repl_mode = flag;
}
```

Apply the option in `constructor()` (around line 714-721):
```zig
transpiler.options.repl_mode = config.repl_mode;
```

### 2. Add Feature Flag to Runtime

**File**: `src/runtime.zig` (in `Runtime.Features`)

```zig
/// REPL mode: transforms code for interactive evaluation
/// - Wraps lone object literals `{...}` in parentheses
/// - Hoists variable declarations for REPL persistence
/// - Wraps last expression in { value: expr } for result capture
/// - Assigns functions to context for persistence
repl_mode: bool = false,
```

### 3. Add to BundleOptions

**File**: `src/options.zig`

Add to `BundleOptions` struct:
```zig
repl_mode: bool = false,
```

### 4. Implement REPL Transforms in Parser

**File**: `src/ast/P.zig`

#### 4a. Object Literal Detection (Parser-Level)

In REPL mode, the parser should prefer interpreting ambiguous `{...}` as object literals instead of blocks.

**Location**: `src/ast/parseStmt.zig` in statement parsing

When `repl_mode` is true and the parser sees `{` at the start of a statement:
1. Try parsing as expression statement (object literal) first
2. If that fails, fall back to block statement

This is similar to how JavaScript engines handle REPL input. The parser already has the infrastructure to do this - we just need to change the precedence in REPL mode.

```zig
// In parseStmt when repl_mode is true and we see '{'
if (p.options.features.repl_mode and p.token.tag == .t_open_brace) {
    // Try parsing as expression first
    const saved_state = p.saveState();
    if (p.tryParseExpressionStatement()) |expr_stmt| {
        return expr_stmt;
    }
    p.restoreState(saved_state);
    // Fall back to block statement
    return p.parseBlockStatement();
}
```

This handles:
- `{a: 1}` → parsed as object literal expression
- `{a: 1, b: 2}` → parsed as object literal expression
- `{ let x = 1; }` → fails as expression, parsed as block
- `{ label: break label; }` → fails as expression (break not valid in object), parsed as block

#### 4b. REPL Transform Pass (in toAST after visiting)

Add a new function `applyReplTransforms()` that:

1. **Detect if transform is needed**: Walk AST to check for top-level `await`
2. **Skip transform when**:
   - No `await` at top level
   - Top-level `return` statement exists
3. **When transform IS needed**:
   - Wrap entire code in `(async () => { ... })()`
   - Hoist variable declarations outside the async wrapper
   - Convert `const` to `let` for persistence
   - Wrap last expression in `return { value: (expr) }`
   - Handle function declarations (assign to `this`)
   - Handle class declarations (hoist as `let`)

**Key Logic:**
```zig
fn applyReplTransforms(p: *Parser, stmts: []Stmt) ![]Stmt {
    // 1. Check for top-level await
    const has_await = p.hasTopLevelAwait(stmts);
    const has_return = p.hasTopLevelReturn(stmts);

    if (!has_await or has_return) {
        // Just wrap last expression, no async wrapper needed
        return p.wrapLastExpression(stmts);
    }

    // 2. Collect declarations to hoist
    var hoisted = std.ArrayList(Stmt).init(p.allocator);
    var inner_stmts = std.ArrayList(Stmt).init(p.allocator);

    for (stmts) |stmt| {
        switch (stmt.data) {
            .s_local => |local| {
                // Hoist declaration, convert const→let
                try hoisted.append(p.createHoistedDecl(local));
                // Add assignment expression to inner
                try inner_stmts.append(p.createAssignmentExpr(local));
            },
            .s_function => |func| {
                // var foo; (hoisted)
                try hoisted.append(p.createVarDecl(func.name));
                // this.foo = foo; function foo() {} (inner)
                try inner_stmts.append(p.createThisAssignment(func.name));
                try inner_stmts.append(stmt);
            },
            .s_class => |class| {
                // let Foo; (hoisted)
                try hoisted.append(p.createLetDecl(class.name));
                // Foo = class Foo {} (inner)
                try inner_stmts.append(p.createClassAssignment(class));
            },
            else => try inner_stmts.append(stmt),
        }
    }

    // 3. Wrap last expression in return { value: expr }
    p.wrapLastExpressionWithReturn(&inner_stmts);

    // 4. Create async IIFE: (async () => { ...inner... })()
    const async_iife = p.createAsyncIIFE(inner_stmts.items);

    // 5. Combine: hoisted declarations + async IIFE
    try hoisted.append(async_iife);
    return hoisted.toOwnedSlice();
}
```

### 5. TypeScript Type Definitions

**File**: `packages/bun-types/bun.d.ts`

Add to `TranspilerOptions` interface (around line 1748):
```typescript
interface TranspilerOptions {
  // ... existing options ...

  /**
   * Enable REPL mode transforms:
   * - Wraps object literals in parentheses
   * - Hoists declarations for REPL persistence
   * - Wraps last expression in { value: expr } for result capture
   * - Wraps code with await in async IIFE
   */
  replMode?: boolean;
}
```

---

## Files to Modify

| File | Changes |
|------|---------|
| `src/bun.js/api/JSTranspiler.zig` | Add `repl_mode` to Config, parse from JS, apply to transpiler |
| `src/runtime.zig` | Add `repl_mode: bool` to `Runtime.Features` |
| `src/options.zig` | Add `repl_mode: bool` to `BundleOptions` |
| `src/ast/P.zig` | REPL transform pass in `toAST()` |
| `src/ast/parseStmt.zig` | Object literal vs block disambiguation in REPL mode |
| `packages/bun-types/bun.d.ts` | Add `replMode?: boolean` to `TranspilerOptions` |

---

## Test Cases

Create test file: `test/js/bun/transpiler/repl-transform.test.ts`

### Part 1: Transform Output Tests (Unit Tests)

Test exact transformation output matches expected patterns:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("Bun.Transpiler replMode - Transform Output", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  // Based on Node.js test-repl-preprocess-top-level-await.js
  const testCases: [string, string | null][] = [
    // No await = null (no async transform, but still expression capture)
    ['0', null],

    // Basic await
    ['await 0', '(async () => { return { value: (await 0) } })()'],
    ['await 0;', '(async () => { return { value: (await 0) }; })()'],
    ['(await 0)', '(async () => { return ({ value: (await 0) }) })()'],

    // No transform for await inside async functions
    ['async function foo() { await 0; }', null],
    ['async () => await 0', null],
    ['class A { async method() { await 0 } }', null],

    // Top-level return = no transform
    ['await 0; return 0;', null],

    // Multiple await - last one gets return wrapper
    ['await 1; await 2;', '(async () => { await 1; return { value: (await 2) }; })()'],

    // Variable hoisting - var
    ['var a = await 1', 'var a; (async () => { void (a = await 1) })()'],

    // Variable hoisting - let
    ['let a = await 1', 'let a; (async () => { void (a = await 1) })()'],

    // Variable hoisting - const becomes let
    ['const a = await 1', 'let a; (async () => { void (a = await 1) })()'],

    // For loop with var - hoist var
    ['for (var i = 0; i < 1; ++i) { await i }',
     'var i; (async () => { for (void (i = 0); i < 1; ++i) { await i } })()'],

    // For loop with let - no hoist
    ['for (let i = 0; i < 1; ++i) { await i }',
     '(async () => { for (let i = 0; i < 1; ++i) { await i } })()'],

    // Destructuring with var
    ['var {a} = {a:1}, [b] = [1], {c:{d}} = {c:{d: await 1}}',
     'var a, b, d; (async () => { void ( ({a} = {a:1}), ([b] = [1]), ({c:{d}} = {c:{d: await 1}})) })()'],

    // Destructuring with let
    ['let [a, b, c] = await ([1, 2, 3])',
     'let a, b, c; (async () => { void ([a, b, c] = await ([1, 2, 3])) })()'],

    // Function declarations - assign to this
    ['await 0; function foo() {}',
     'var foo; (async () => { await 0; this.foo = foo; function foo() {} })()'],

    // Class declarations - hoist as let
    ['await 0; class Foo {}',
     'let Foo; (async () => { await 0; Foo=class Foo {} })()'],

    // Nested scopes
    ['if (await true) { var a = 1; }',
     'var a; (async () => { if (await true) { void (a = 1); } })()'],
    ['if (await true) { let a = 1; }',
     '(async () => { if (await true) { let a = 1; } })()'],

    // Mixed declarations
    ['var a = await 1; let b = 2; const c = 3;',
     'var a; let b; let c; (async () => { void (a = await 1); void (b = 2); void (c = 3); })()'],

    // for await
    ['for await (var i of asyncIterable) { i; }',
     'var i; (async () => { for await (i of asyncIterable) { i; } })()'],

    // for-of with var
    ['for (var i of [1,2,3]) { await 1; }',
     'var i; (async () => { for (i of [1,2,3]) { await 1; } })()'],

    // for-in with var
    ['for (var i in {x:1}) { await 1 }',
     'var i; (async () => { for (i in {x:1}) { await 1 } })()'],

    // Spread in destructuring
    ['var { ...rest } = await {}',
     'var rest; (async () => { void ({ ...rest } = await {}) })()'],
  ];

  for (const [input, expected] of testCases) {
    test(`transform: ${input.slice(0, 40)}...`, () => {
      const result = transpiler.transformSync(input);
      if (expected === null) {
        // No async transform expected, but expression capture may still happen
        expect(result).not.toMatch(/^\(async/);
      } else {
        expect(result.trim()).toBe(expected);
      }
    });
  }

  // Object literal detection - parser handles this automatically in REPL mode
  describe("object literal vs block disambiguation", () => {
    test("{a: 1} parsed as object literal", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{a: 1}");
      const result = await vm.runInContext(code, ctx);
      // Should evaluate to object, not undefined (block with label)
      expect(result.value).toEqual({ a: 1 });
    });

    test("{a: 1, b: 2} parsed as object literal", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{a: 1, b: 2}");
      const result = await vm.runInContext(code, ctx);
      expect(result.value).toEqual({ a: 1, b: 2 });
    });

    test("{ let x = 1; x } parsed as block", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{ let x = 1; x }");
      const result = await vm.runInContext(code, ctx);
      // Block returns last expression value
      expect(result.value).toBe(1);
    });

    test("{ x: 1; y: 2 } parsed as block with labels", async () => {
      // Semicolons make this a block with labeled statements, not object
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{ x: 1; y: 2 }");
      const result = await vm.runInContext(code, ctx);
      // Block with labels returns last value
      expect(result.value).toBe(2);
    });
  });
});
```

### Part 2: Variable Persistence Tests (Integration with node:vm)

Test that variables persist across multiple REPL evaluations:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL Variable Persistence", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  // Helper to run multiple REPL lines in sequence
  async function runReplSession(lines: string[], context?: object) {
    const ctx = vm.createContext(context ?? { console });
    const results: any[] = [];

    for (const line of lines) {
      const transformed = transpiler.transformSync(line);
      const result = await vm.runInContext(transformed, ctx);
      results.push(result?.value ?? result);
    }

    return { results, context: ctx };
  }

  test("var persists across lines", async () => {
    const { results, context } = await runReplSession([
      "var x = 10",
      "x + 5",
      "x = 20",
      "x",
    ]);

    expect(results[1]).toBe(15);  // x + 5
    expect(results[3]).toBe(20);  // x after reassignment
    expect(context.x).toBe(20);   // x visible in context
  });

  test("let persists across lines (hoisted)", async () => {
    const { results } = await runReplSession([
      "let y = await Promise.resolve(100)",
      "y * 2",
    ]);

    expect(results[1]).toBe(200);
  });

  test("const becomes let, can be reassigned in later lines", async () => {
    const { results } = await runReplSession([
      "const z = await Promise.resolve(5)",
      "z",
      // In REPL, const becomes let, so next line can redeclare
      "z = 10",  // This works because const→let
      "z",
    ]);

    expect(results[1]).toBe(5);
    expect(results[3]).toBe(10);
  });

  test("function declarations persist", async () => {
    const { results, context } = await runReplSession([
      "await 1; function add(a, b) { return a + b; }",
      "add(2, 3)",
      "function multiply(a, b) { return a * b; }",  // no await
      "multiply(4, 5)",
    ]);

    expect(results[1]).toBe(5);
    expect(results[3]).toBe(20);
    expect(typeof context.add).toBe("function");
    expect(typeof context.multiply).toBe("function");
  });

  test("class declarations persist", async () => {
    const { results, context } = await runReplSession([
      "await 1; class Counter { constructor() { this.count = 0; } inc() { this.count++; } }",
      "const c = new Counter()",
      "c.inc(); c.inc(); c.count",
    ]);

    expect(results[2]).toBe(2);
    expect(typeof context.Counter).toBe("function");
  });

  test("complex session with mixed declarations", async () => {
    const { results } = await runReplSession([
      "var total = 0",
      "async function addAsync(n) { return total += await Promise.resolve(n); }",
      "await addAsync(10)",
      "await addAsync(20)",
      "total",
    ]);

    expect(results[2]).toBe(10);
    expect(results[3]).toBe(30);
    expect(results[4]).toBe(30);
  });

  test("destructuring assignment persists", async () => {
    const { results, context } = await runReplSession([
      "var { a, b } = await Promise.resolve({ a: 1, b: 2 })",
      "a + b",
      "var [x, y, z] = [10, 20, 30]",
      "x + y + z",
    ]);

    expect(results[1]).toBe(3);
    expect(results[3]).toBe(60);
    expect(context.a).toBe(1);
    expect(context.x).toBe(10);
  });
});
```

### Part 3: eval() Scoping Semantics Tests

Test that REPL behaves like eval() with proper scoping:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL eval() Scoping Semantics", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  test("var hoists to global context", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("var globalVar = 42");
    await vm.runInContext(code, ctx);

    expect(ctx.globalVar).toBe(42);
  });

  test("let/const hoisted for REPL but scoped correctly", async () => {
    const ctx = vm.createContext({});

    // With await, let is hoisted outside async wrapper
    const code1 = transpiler.transformSync("let x = await 1");
    await vm.runInContext(code1, ctx);
    expect(ctx.x).toBe(1);

    // Without await, let behavior depends on implementation
    const code2 = transpiler.transformSync("let y = 2");
    await vm.runInContext(code2, ctx);
    // y should still be accessible in REPL context
  });

  test("block-scoped let does NOT leak", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("if (await true) { let blockScoped = 1; }");
    await vm.runInContext(code, ctx);

    // blockScoped should NOT be visible in context
    expect(ctx.blockScoped).toBeUndefined();
  });

  test("function in block hoists with var (sloppy mode)", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("if (await true) { function blockFn() { return 42; } }");
    await vm.runInContext(code, ctx);

    // In sloppy mode, function in block hoists to function scope
    expect(typeof ctx.blockFn).toBe("function");
    expect(ctx.blockFn()).toBe(42);
  });

  test("this binding in function declarations", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("await 1; function greet() { return 'hello'; }");
    await vm.runInContext(code, ctx);

    // Function should be assigned to this (context) for REPL persistence
    expect(ctx.greet()).toBe("hello");
  });

  test("async function expression captures result", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("await (async () => { return 42; })()");
    const result = await vm.runInContext(code, ctx);

    expect(result.value).toBe(42);
  });

  test("Promise result NOT auto-awaited due to { value: } wrapper", async () => {
    const ctx = vm.createContext({});

    // Without wrapper, result would be auto-awaited
    // With { value: } wrapper, we get the Promise object
    const code = transpiler.transformSync("await Promise.resolve(Promise.resolve(42))");
    const result = await vm.runInContext(code, ctx);

    // The inner Promise should be in value, not auto-resolved
    expect(result.value).toBeInstanceOf(Promise);
    expect(await result.value).toBe(42);
  });
});
```

### Part 4: Edge Cases and Error Handling

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL Edge Cases", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  test("empty input", () => {
    const result = transpiler.transformSync("");
    expect(result).toBe("");
  });

  test("whitespace only", () => {
    const result = transpiler.transformSync("   \n\t  ");
    expect(result.trim()).toBe("");
  });

  test("comment only", () => {
    const result = transpiler.transformSync("// just a comment");
    expect(result).toContain("// just a comment");
  });

  test("multiline input", () => {
    const input = `
      var x = await 1;
      var y = await 2;
      x + y
    `;
    const result = transpiler.transformSync(input);
    expect(result).toContain("var x");
    expect(result).toContain("var y");
    expect(result).toContain("async");
  });

  test("TypeScript syntax", () => {
    const input = "const x: number = await Promise.resolve(42)";
    const result = transpiler.transformSync(input);
    expect(result).not.toContain(": number"); // Types stripped
    expect(result).toContain("let x");
  });

  test("JSX in REPL", () => {
    const input = "await Promise.resolve(<div>Hello</div>)";
    const result = transpiler.transformSync(input);
    expect(result).toContain("async");
  });

  test("import expression (dynamic)", () => {
    // Dynamic imports should work fine
    const input = "await import('fs')";
    const result = transpiler.transformSync(input);
    expect(result).toContain("import");
  });

  test("nested await expressions", () => {
    const input = "await (await Promise.resolve(Promise.resolve(1)))";
    const result = transpiler.transformSync(input);
    expect(result).toContain("{ value:");
  });

  test("for-await-of", () => {
    const input = `
      async function* gen() { yield 1; yield 2; }
      for await (const x of gen()) { console.log(x); }
    `;
    const result = transpiler.transformSync(input);
    expect(result).toContain("var gen");
    expect(result).toContain("for await");
  });
});
```

---

## Verification Plan

### 1. Build and Basic Tests
```bash
# Build Bun with changes
bun bd

# Run the REPL transform tests
bun bd test test/js/bun/transpiler/repl-transform.test.ts
```

### 2. Manual Transform Output Verification
```typescript
// test-repl-manual.ts
const t = new Bun.Transpiler({ loader: "tsx", replMode: true });

// Object literal
console.log("Object literal:");
console.log(t.transformSync("{a: 1}"));
// Expected: contains "({a: 1})"

// Basic await
console.log("\nBasic await:");
console.log(t.transformSync("await 0"));
// Expected: (async () => { return { value: (await 0) } })()

// Variable hoisting
console.log("\nVar hoisting:");
console.log(t.transformSync("var x = await 1"));
// Expected: var x; (async () => { void (x = await 1) })()

// const → let
console.log("\nConst to let:");
console.log(t.transformSync("const x = await 1"));
// Expected: let x; (async () => { void (x = await 1) })()

// Function hoisting
console.log("\nFunction:");
console.log(t.transformSync("await 0; function foo() {}"));
// Expected: var foo; (async () => { await 0; this.foo = foo; function foo() {} })()
```

### 3. Full REPL Session Simulation
```typescript
// test-repl-session.ts
import vm from "node:vm";

const t = new Bun.Transpiler({ loader: "tsx", replMode: true });
const ctx = vm.createContext({ console, Promise });

async function repl(code: string) {
  const transformed = t.transformSync(code);
  console.log(`> ${code}`);
  console.log(`[transformed]: ${transformed}`);
  const result = await vm.runInContext(transformed, ctx);
  console.log(`= ${JSON.stringify(result?.value ?? result)}\n`);
  return result?.value ?? result;
}

// Test session
await repl("var counter = 0");
await repl("function increment() { return ++counter; }");
await repl("increment()");  // Should be 1
await repl("increment()");  // Should be 2
await repl("counter");      // Should be 2

await repl("const data = await Promise.resolve({ x: 10, y: 20 })");
await repl("data.x + data.y");  // Should be 30

await repl("class Point { constructor(x, y) { this.x = x; this.y = y; } }");
await repl("const p = new Point(3, 4)");
await repl("Math.sqrt(p.x**2 + p.y**2)");  // Should be 5
```

### 4. Verify No Regressions
```bash
# Run existing transpiler tests
bun bd test test/js/bun/transpiler/

# Run existing vm tests
bun bd test test/js/node/vm/
```

### 5. Cross-check with Node.js (Optional)
Compare transform outputs with Node.js's `processTopLevelAwait`:
```typescript
// Compare a few key transforms with Node.js output
const cases = [
  "await 0",
  "var x = await 1",
  "await 0; function foo() {}",
];
// Verify Bun output matches Node.js patterns
```
</claude-plan>
2026-01-19 00:22:36 -08:00
14 changed files with 3060 additions and 8 deletions

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@
.envrc
.eslintcache
.gdb_history
.bun_repl_history
.idea
.next
.ninja_deps

View File

@@ -1745,6 +1745,17 @@ declare module "bun" {
* @default "warn"
*/
logLevel?: "verbose" | "debug" | "info" | "warn" | "error";
/**
* Enable REPL mode transforms:
* - Wraps top-level inputs that appear to be object literals (inputs starting with '{' without trailing ';') in parentheses
* - Hoists all declarations as var for REPL persistence across vm.runInContext calls
* - Wraps last expression in { __proto__: null, value: expr } for result capture
* - Wraps code in sync/async IIFE to avoid parentheses around object literals
*
* @default false
*/
replMode?: boolean;
}
/**

View File

@@ -6467,6 +6467,11 @@ pub fn NewParser_(
parts.items[0].stmts = top_level_stmts;
}
// REPL mode transforms
if (p.options.repl_mode) {
try repl_transforms.ReplTransforms(P).apply(p, parts, allocator);
}
var top_level_symbols_to_parts = js_ast.Ast.TopLevelSymbolToParts{};
var top_level = &top_level_symbols_to_parts;
@@ -6760,6 +6765,8 @@ var falseValueExpr = Expr.Data{ .e_boolean = E.Boolean{ .value = false } };
const string = []const u8;
const repl_transforms = @import("./repl_transforms.zig");
const Define = @import("../defines.zig").Define;
const DefineData = @import("../defines.zig").DefineData;

View File

@@ -38,6 +38,13 @@ pub const Parser = struct {
/// able to customize what import sources are used.
framework: ?*bun.bake.Framework = null,
/// REPL mode: transforms code for interactive evaluation
/// - Wraps lone object literals `{...}` in parentheses
/// - Hoists variable declarations for REPL persistence
/// - Wraps last expression in { value: expr } for result capture
/// - Wraps code with await in async IIFE
repl_mode: bool = false,
pub fn hashForRuntimeTranspiler(this: *const Options, hasher: *std.hash.Wyhash, did_use_jsx: bool) void {
bun.assert(!this.bundle);

365
src/ast/repl_transforms.zig Normal file
View File

@@ -0,0 +1,365 @@
/// REPL Transform module - transforms code for interactive REPL evaluation
///
/// This module provides transformations for REPL mode:
/// - Wraps the last expression in { value: expr } for result capture
/// - Wraps code with await in async IIFE with variable hoisting
/// - Hoists declarations for variable persistence across REPL lines
pub fn ReplTransforms(comptime P: type) type {
return struct {
const Self = @This();
/// Apply REPL-mode transforms to the AST.
/// This transforms code for interactive evaluation:
/// - Wraps the last expression in { value: expr } for result capture
/// - Wraps code with await in async IIFE with variable hoisting
pub fn apply(p: *P, parts: *ListManaged(js_ast.Part), allocator: Allocator) !void {
// Skip transform if there's a top-level return (indicates module pattern)
if (p.has_top_level_return) {
return;
}
// Collect all statements
var total_stmts_count: usize = 0;
for (parts.items) |part| {
total_stmts_count += part.stmts.len;
}
if (total_stmts_count == 0) {
return;
}
// Check if there's top-level await
const has_top_level_await = p.top_level_await_keyword.len > 0;
// Collect all statements into a single array
var all_stmts = bun.handleOom(allocator.alloc(Stmt, total_stmts_count));
var stmt_idx: usize = 0;
for (parts.items) |part| {
for (part.stmts) |stmt| {
all_stmts[stmt_idx] = stmt;
stmt_idx += 1;
}
}
// Apply transform with is_async based on presence of top-level await
try transformWithHoisting(p, parts, all_stmts, allocator, has_top_level_await);
}
/// Transform code with hoisting and IIFE wrapper
/// @param is_async: true for async IIFE (when top-level await present), false for sync IIFE
fn transformWithHoisting(
p: *P,
parts: *ListManaged(js_ast.Part),
all_stmts: []Stmt,
allocator: Allocator,
is_async: bool,
) !void {
if (all_stmts.len == 0) return;
// Lists for hoisted declarations and inner statements
var hoisted_stmts = ListManaged(Stmt).init(allocator);
var inner_stmts = ListManaged(Stmt).init(allocator);
try hoisted_stmts.ensureTotalCapacity(all_stmts.len);
try inner_stmts.ensureTotalCapacity(all_stmts.len);
// Process each statement - hoist all declarations for REPL persistence
for (all_stmts) |stmt| {
switch (stmt.data) {
.s_local => |local| {
// Hoist all declarations as var so they become context properties
// In sloppy mode, var at top level becomes a property of the global/context object
// This is essential for REPL variable persistence across vm.runInContext calls
const kind: S.Local.Kind = .k_var;
// Extract individual identifiers from binding patterns for hoisting
var hoisted_decl_list = ListManaged(G.Decl).init(allocator);
for (local.decls.slice()) |decl| {
try extractIdentifiersFromBinding(p, decl.binding, &hoisted_decl_list);
}
if (hoisted_decl_list.items.len > 0) {
try hoisted_stmts.append(p.s(S.Local{
.kind = kind,
.decls = Decl.List.fromOwnedSlice(hoisted_decl_list.items),
}, stmt.loc));
}
// Create assignment expressions for the inner statements
for (local.decls.slice()) |decl| {
if (decl.value) |value| {
// Create assignment expression: binding = value
const assign_expr = createBindingAssignment(p, decl.binding, value, allocator);
try inner_stmts.append(p.s(S.SExpr{ .value = assign_expr }, stmt.loc));
}
}
},
.s_function => |func| {
// For function declarations:
// Hoist as: var funcName;
// Inner: this.funcName = funcName; function funcName() {}
if (func.func.name) |name_loc| {
try hoisted_stmts.append(p.s(S.Local{
.kind = .k_var,
.decls = Decl.List.fromOwnedSlice(bun.handleOom(allocator.dupe(G.Decl, &.{
G.Decl{
.binding = p.b(B.Identifier{ .ref = name_loc.ref.? }, name_loc.loc),
.value = null,
},
}))),
}, stmt.loc));
// Add this.funcName = funcName assignment
const this_expr = p.newExpr(E.This{}, stmt.loc);
const this_dot = p.newExpr(E.Dot{
.target = this_expr,
.name = p.symbols.items[name_loc.ref.?.innerIndex()].original_name,
.name_loc = name_loc.loc,
}, stmt.loc);
const func_id = p.newExpr(E.Identifier{ .ref = name_loc.ref.? }, name_loc.loc);
const assign = p.newExpr(E.Binary{
.op = .bin_assign,
.left = this_dot,
.right = func_id,
}, stmt.loc);
try inner_stmts.append(p.s(S.SExpr{ .value = assign }, stmt.loc));
}
// Add the function declaration itself
try inner_stmts.append(stmt);
},
.s_class => |class| {
// For class declarations:
// Hoist as: var ClassName; (use var so it persists to vm context)
// Inner: ClassName = class ClassName {}
if (class.class.class_name) |name_loc| {
try hoisted_stmts.append(p.s(S.Local{
.kind = .k_var,
.decls = Decl.List.fromOwnedSlice(bun.handleOom(allocator.dupe(G.Decl, &.{
G.Decl{
.binding = p.b(B.Identifier{ .ref = name_loc.ref.? }, name_loc.loc),
.value = null,
},
}))),
}, stmt.loc));
// Convert class declaration to assignment: ClassName = class ClassName {}
const class_expr = p.newExpr(class.class, stmt.loc);
const class_id = p.newExpr(E.Identifier{ .ref = name_loc.ref.? }, name_loc.loc);
const assign = p.newExpr(E.Binary{
.op = .bin_assign,
.left = class_id,
.right = class_expr,
}, stmt.loc);
try inner_stmts.append(p.s(S.SExpr{ .value = assign }, stmt.loc));
} else {
try inner_stmts.append(stmt);
}
},
.s_directive => |directive| {
// In REPL mode, treat directives (string literals) as expressions
const str_expr = p.newExpr(E.String{ .data = directive.value }, stmt.loc);
try inner_stmts.append(p.s(S.SExpr{ .value = str_expr }, stmt.loc));
},
else => {
try inner_stmts.append(stmt);
},
}
}
// Wrap the last expression in return { value: expr }
wrapLastExpressionWithReturn(p, &inner_stmts, allocator);
// Create the IIFE: (() => { ...inner_stmts... })() or (async () => { ... })()
const arrow = p.newExpr(E.Arrow{
.args = &.{},
.body = .{ .loc = logger.Loc.Empty, .stmts = inner_stmts.items },
.is_async = is_async,
}, logger.Loc.Empty);
const iife = p.newExpr(E.Call{
.target = arrow,
.args = ExprNodeList{},
}, logger.Loc.Empty);
// Final output: hoisted declarations + IIFE call
const final_stmts_count = hoisted_stmts.items.len + 1;
var final_stmts = bun.handleOom(allocator.alloc(Stmt, final_stmts_count));
for (hoisted_stmts.items, 0..) |stmt, j| {
final_stmts[j] = stmt;
}
final_stmts[hoisted_stmts.items.len] = p.s(S.SExpr{ .value = iife }, logger.Loc.Empty);
// Update parts
if (parts.items.len > 0) {
parts.items[0].stmts = final_stmts;
parts.items.len = 1;
}
}
/// Wrap the last expression in return { value: expr }
fn wrapLastExpressionWithReturn(p: *P, inner_stmts: *ListManaged(Stmt), allocator: Allocator) void {
if (inner_stmts.items.len > 0) {
var last_idx: usize = inner_stmts.items.len;
while (last_idx > 0) {
last_idx -= 1;
const last_stmt = inner_stmts.items[last_idx];
switch (last_stmt.data) {
.s_empty, .s_comment => continue,
.s_expr => |expr_data| {
// Wrap in return { value: expr }
const wrapped = wrapExprInValueObject(p, expr_data.value, allocator);
inner_stmts.items[last_idx] = p.s(S.Return{ .value = wrapped }, last_stmt.loc);
break;
},
else => break,
}
}
}
}
/// Extract individual identifiers from a binding pattern for hoisting
fn extractIdentifiersFromBinding(p: *P, binding: Binding, decls: *ListManaged(G.Decl)) !void {
switch (binding.data) {
.b_identifier => |ident| {
try decls.append(G.Decl{
.binding = p.b(B.Identifier{ .ref = ident.ref }, binding.loc),
.value = null,
});
},
.b_array => |arr| {
for (arr.items) |item| {
try extractIdentifiersFromBinding(p, item.binding, decls);
}
},
.b_object => |obj| {
for (obj.properties) |prop| {
try extractIdentifiersFromBinding(p, prop.value, decls);
}
},
.b_missing => {},
}
}
/// Create { __proto__: null, value: expr } wrapper object
/// Uses null prototype to create a clean data object
fn wrapExprInValueObject(p: *P, expr: Expr, allocator: Allocator) Expr {
var properties = bun.handleOom(allocator.alloc(G.Property, 2));
// __proto__: null - creates null-prototype object
properties[0] = G.Property{
.key = p.newExpr(E.String{ .data = "__proto__" }, expr.loc),
.value = p.newExpr(E.Null{}, expr.loc),
};
// value: expr - the actual result value
properties[1] = G.Property{
.key = p.newExpr(E.String{ .data = "value" }, expr.loc),
.value = expr,
};
return p.newExpr(E.Object{
.properties = G.Property.List.fromOwnedSlice(properties),
}, expr.loc);
}
/// Create assignment expression from binding pattern
fn createBindingAssignment(p: *P, binding: Binding, value: Expr, allocator: Allocator) Expr {
switch (binding.data) {
.b_identifier => |ident| {
return p.newExpr(E.Binary{
.op = .bin_assign,
.left = p.newExpr(E.Identifier{ .ref = ident.ref }, binding.loc),
.right = value,
}, binding.loc);
},
.b_array => {
// For array destructuring, create: [a, b] = value
return p.newExpr(E.Binary{
.op = .bin_assign,
.left = convertBindingToExpr(p, binding, allocator),
.right = value,
}, binding.loc);
},
.b_object => {
// For object destructuring, create: {a, b} = value
return p.newExpr(E.Binary{
.op = .bin_assign,
.left = convertBindingToExpr(p, binding, allocator),
.right = value,
}, binding.loc);
},
.b_missing => {
// Return Missing expression to match convertBindingToExpr
return p.newExpr(E.Missing{}, binding.loc);
},
}
}
/// Convert a binding pattern to an expression (for assignment targets)
/// Handles spread/rest patterns in arrays and objects to match Binding.toExpr behavior
fn convertBindingToExpr(p: *P, binding: Binding, allocator: Allocator) Expr {
switch (binding.data) {
.b_identifier => |ident| {
return p.newExpr(E.Identifier{ .ref = ident.ref }, binding.loc);
},
.b_array => |arr| {
var items = bun.handleOom(allocator.alloc(Expr, arr.items.len));
for (arr.items, 0..) |item, i| {
const expr = convertBindingToExpr(p, item.binding, allocator);
// Check for spread pattern: if has_spread and this is the last element
if (arr.has_spread and i == arr.items.len - 1) {
items[i] = p.newExpr(E.Spread{ .value = expr }, expr.loc);
} else if (item.default_value) |default_val| {
items[i] = p.newExpr(E.Binary{
.op = .bin_assign,
.left = expr,
.right = default_val,
}, item.binding.loc);
} else {
items[i] = expr;
}
}
return p.newExpr(E.Array{
.items = ExprNodeList.fromOwnedSlice(items),
.is_single_line = arr.is_single_line,
}, binding.loc);
},
.b_object => |obj| {
var properties = bun.handleOom(allocator.alloc(G.Property, obj.properties.len));
for (obj.properties, 0..) |prop, i| {
properties[i] = G.Property{
.flags = prop.flags,
.key = prop.key,
// Set kind to .spread if the property has spread flag
.kind = if (prop.flags.contains(.is_spread)) .spread else .normal,
.value = convertBindingToExpr(p, prop.value, allocator),
.initializer = prop.default_value,
};
}
return p.newExpr(E.Object{
.properties = G.Property.List.fromOwnedSlice(properties),
.is_single_line = obj.is_single_line,
}, binding.loc);
},
.b_missing => {
return p.newExpr(E.Missing{}, binding.loc);
},
}
}
};
}
const std = @import("std");
const Allocator = std.mem.Allocator;
const ListManaged = std.array_list.Managed;
const bun = @import("bun");
const logger = bun.logger;
const js_ast = bun.ast;
const B = js_ast.B;
const Binding = js_ast.Binding;
const E = js_ast.E;
const Expr = js_ast.Expr;
const ExprNodeList = js_ast.ExprNodeList;
const S = js_ast.S;
const Stmt = js_ast.Stmt;
const G = js_ast.G;
const Decl = G.Decl;

View File

@@ -42,6 +42,7 @@ pub const Config = struct {
minify_identifiers: bool = false,
minify_syntax: bool = false,
no_macros: bool = false,
repl_mode: bool = false,
pub fn fromJS(this: *Config, globalThis: *jsc.JSGlobalObject, object: jsc.JSValue, allocator: std.mem.Allocator) bun.JSError!void {
if (object.isUndefinedOrNull()) {
@@ -245,6 +246,10 @@ pub const Config = struct {
this.dead_code_elimination = flag;
}
if (try object.getBooleanLoose(globalThis, "replMode")) |flag| {
this.repl_mode = flag;
}
if (try object.getTruthy(globalThis, "minify")) |minify| {
if (minify.isBoolean()) {
this.minify_whitespace = minify.toBoolean();
@@ -698,7 +703,8 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) b
transpiler.options.macro_remap = config.macro_map;
}
transpiler.options.dead_code_elimination = config.dead_code_elimination;
// REPL mode disables DCE to preserve expressions like `42`
transpiler.options.dead_code_elimination = config.dead_code_elimination and !config.repl_mode;
transpiler.options.minify_whitespace = config.minify_whitespace;
// Keep defaults for these
@@ -717,6 +723,7 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) b
transpiler.options.inlining = config.runtime.inlining;
transpiler.options.hot_module_reloading = config.runtime.hot_module_reloading;
transpiler.options.react_fast_refresh = false;
transpiler.options.repl_mode = config.repl_mode;
return this;
}
@@ -738,9 +745,47 @@ pub fn deinit(this: *JSTranspiler) void {
bun.destroy(this);
}
/// Check if code looks like an object literal that would be misinterpreted as a block
/// Returns true if code starts with { (after whitespace) and doesn't end with ;
/// This matches Node.js REPL behavior for object literal disambiguation
fn isLikelyObjectLiteral(code: []const u8) bool {
// Skip leading whitespace
var start: usize = 0;
while (start < code.len and (code[start] == ' ' or code[start] == '\t' or code[start] == '\n' or code[start] == '\r')) {
start += 1;
}
// Check if starts with {
if (start >= code.len or code[start] != '{') {
return false;
}
// Skip trailing whitespace
var end: usize = code.len;
while (end > 0 and (code[end - 1] == ' ' or code[end - 1] == '\t' or code[end - 1] == '\n' or code[end - 1] == '\r')) {
end -= 1;
}
// Check if ends with semicolon - if so, it's likely a block statement
if (end > 0 and code[end - 1] == ';') {
return false;
}
return true;
}
fn getParseResult(this: *JSTranspiler, allocator: std.mem.Allocator, code: []const u8, loader: ?Loader, macro_js_ctx: Transpiler.MacroJSValueType) ?Transpiler.ParseResult {
const name = this.config.default_loader.stdinName();
const source = &logger.Source.initPathString(name, code);
// In REPL mode, wrap potential object literals in parentheses
// If code starts with { and doesn't end with ; it might be an object literal
// that would otherwise be parsed as a block statement
const processed_code: []const u8 = if (this.config.repl_mode and isLikelyObjectLiteral(code))
std.fmt.allocPrint(allocator, "({s})", .{code}) catch code
else
code;
const source = &logger.Source.initPathString(name, processed_code);
const jsx = if (this.config.tsconfig != null)
this.config.tsconfig.?.mergeJSX(this.transpiler.options.jsx)

View File

@@ -92,6 +92,7 @@ pub const AuditCommand = @import("./cli/audit_command.zig").AuditCommand;
pub const InitCommand = @import("./cli/init_command.zig").InitCommand;
pub const WhyCommand = @import("./cli/why_command.zig").WhyCommand;
pub const FuzzilliCommand = @import("./cli/fuzzilli_command.zig").FuzzilliCommand;
pub const ReplCommand = @import("./cli/repl_command.zig");
pub const Arguments = @import("./cli/Arguments.zig");
@@ -826,12 +827,8 @@ pub const Command = struct {
return;
},
.ReplCommand => {
// TODO: Put this in native code.
var ctx = try Command.init(allocator, log, .BunxCommand);
ctx.debug.run_in_bun = true; // force the same version of bun used. fixes bun-debug for example
var args = bun.argv[0..];
args[1] = "bun-repl";
try BunxCommand.exec(ctx, args);
const ctx = try Command.init(allocator, log, .ReplCommand);
try ReplCommand.exec(ctx);
return;
},
.RemoveCommand => {

1628
src/cli/repl_command.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1802,6 +1802,10 @@ pub const BundleOptions = struct {
minify_identifiers: bool = false,
keep_names: bool = false,
dead_code_elimination: bool = true,
/// REPL mode: transforms code for interactive evaluation with vm.runInContext.
/// Hoists declarations as var for persistence, wraps code in IIFE, and
/// captures the last expression in { value: expr } for result extraction.
repl_mode: bool = false,
css_chunking: bool,
ignore_dce_annotations: bool = false,

View File

@@ -215,6 +215,13 @@ pub const Runtime = struct {
/// When `feature("FLAG_NAME")` is called, it returns true if FLAG_NAME is in this set.
bundler_feature_flags: *const bun.StringSet = &empty_bundler_feature_flags,
/// REPL mode: transforms code for interactive evaluation
/// - Wraps lone object literals `{...}` in parentheses
/// - Hoists variable declarations for REPL persistence
/// - Wraps last expression in { value: expr } for result capture
/// - Assigns functions to context for persistence
repl_mode: bool = false,
pub const empty_bundler_feature_flags: bun.StringSet = bun.StringSet.initComptime();
/// Initialize bundler feature flags for dead-code elimination via `import { feature } from "bun:bundle"`.

View File

@@ -1115,6 +1115,8 @@ pub const Transpiler = struct {
opts.features.dead_code_elimination = transpiler.options.dead_code_elimination;
opts.features.remove_cjs_module_wrapper = this_parse.remove_cjs_module_wrapper;
opts.features.bundler_feature_flags = transpiler.options.bundler_feature_flags;
opts.features.repl_mode = transpiler.options.repl_mode;
opts.repl_mode = transpiler.options.repl_mode;
if (transpiler.macro_context == null) {
transpiler.macro_context = js_ast.Macro.MacroContext.init(transpiler);

362
test/cli/repl.test.ts Normal file
View File

@@ -0,0 +1,362 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
describe("bun repl", () => {
test("evaluates simple expressions", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("1 + 2\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("3");
expect(exitCode).toBe(0);
});
test("supports Bun globals", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("typeof Bun\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("object");
expect(exitCode).toBe(0);
});
test("Bun.version is available", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("Bun.version\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Should contain a version string
expect(stdout).toMatch(/\d+\.\d+\.\d+/);
expect(exitCode).toBe(0);
});
test("let declarations persist across lines", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("let x = 5\n");
proc.stdin.write("x * 2\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("10");
expect(exitCode).toBe(0);
});
test("const declarations persist across lines", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("const y = 7\n");
proc.stdin.write("y + 3\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("10");
expect(exitCode).toBe(0);
});
test("function declarations persist", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("function add(a, b) { return a + b }\n");
proc.stdin.write("add(2, 3)\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("5");
expect(exitCode).toBe(0);
});
test(".help command works", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write(".help\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("REPL Commands");
expect(stdout).toContain(".exit");
expect(stdout).toContain(".clear");
expect(exitCode).toBe(0);
});
test(".timing command toggles timing display", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write(".timing\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("Timing");
expect(exitCode).toBe(0);
});
test("error handling shows error message", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("throw new Error('test error')\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Error should be displayed in stderr
const output = stdout + stderr;
expect(output).toContain("test error");
expect(exitCode).toBe(0);
});
test("object literals are displayed", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("({ foo: 'bar' })\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("foo");
expect(stdout).toContain("bar");
expect(exitCode).toBe(0);
});
test("arrays are displayed", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("[1, 2, 3]\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("1");
expect(stdout).toContain("2");
expect(stdout).toContain("3");
expect(exitCode).toBe(0);
});
test("undefined is not printed for statements", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("let z = 10\n");
proc.stdin.write("z\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// The second line should show 10
expect(stdout).toContain("10");
expect(exitCode).toBe(0);
});
test("multiline input with semicolons", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("var a = 1; var b = 2; a + b\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("3");
expect(exitCode).toBe(0);
});
test("async/await works", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("await Promise.resolve(42)\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("42");
expect(exitCode).toBe(0);
});
test("class declarations persist", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("class Calculator { add(a, b) { return a + b } }\n");
proc.stdin.write("new Calculator().add(3, 7)\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("10");
expect(exitCode).toBe(0);
});
test("destructuring works", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("const { a, b } = { a: 1, b: 2 }\n");
proc.stdin.write("a + b\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("3");
expect(exitCode).toBe(0);
});
test("shell command syntax works", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("$`echo hello from shell`\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("hello from shell");
expect(exitCode).toBe(0);
});
test("Bun.$ template literal works", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("await Bun.$`echo test output`\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("test output");
expect(exitCode).toBe(0);
});
test("Bun.sleep works", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
proc.stdin.write("await Bun.sleep(10)\n");
proc.stdin.write("'slept'\n");
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("slept");
expect(exitCode).toBe(0);
});
});

325
test/cli/repl/repl.test.ts Normal file
View File

@@ -0,0 +1,325 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isWindows } from "harness";
// Helper to run REPL with piped input and capture output
async function runRepl(input: string): Promise<{ stdout: string; stderr: string; exitCode: number }> {
await using proc = Bun.spawn([bunExe(), "repl"], {
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
// Write input to stdin
proc.stdin.write(input);
proc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { stdout, stderr, exitCode };
}
describe.todoIf(isWindows)("bun repl", () => {
test("exits cleanly with .exit", async () => {
const { exitCode } = await runRepl(".exit\n");
expect(exitCode).toBe(0);
});
test(".help command shows help text", async () => {
const { stdout, exitCode } = await runRepl(".help\n.exit\n");
expect(stdout).toContain("REPL");
expect(stdout).toContain(".exit");
expect(exitCode).toBe(0);
});
test(".q is an alias for .exit", async () => {
const { exitCode } = await runRepl(".q\n");
expect(exitCode).toBe(0);
});
test("EOF (empty input) exits the REPL", async () => {
// Empty input (immediate EOF) should exit gracefully
const { exitCode } = await runRepl("");
expect(exitCode).toBe(0);
});
test("evaluates simple expression", async () => {
const { stdout, exitCode } = await runRepl("1 + 1\n.exit\n");
expect(stdout).toContain("2");
expect(exitCode).toBe(0);
});
test("evaluates string expression", async () => {
const { stdout, exitCode } = await runRepl('"hello"\n.exit\n');
expect(stdout).toContain("hello");
expect(exitCode).toBe(0);
});
test("evaluates object literal", async () => {
const { stdout, exitCode } = await runRepl("({a: 1, b: 2})\n.exit\n");
// Should show object representation
expect(stdout).toMatch(/a.*:.*1/);
expect(stdout).toMatch(/b.*:.*2/);
expect(exitCode).toBe(0);
});
test("evaluates array literal", async () => {
const { stdout, exitCode } = await runRepl("[1, 2, 3]\n.exit\n");
expect(stdout).toContain("1");
expect(stdout).toContain("2");
expect(stdout).toContain("3");
expect(exitCode).toBe(0);
});
test("handles console.log", async () => {
const { stdout, exitCode } = await runRepl('console.log("hello world")\n.exit\n');
expect(stdout).toContain("hello world");
expect(exitCode).toBe(0);
});
test("persists variables across lines", async () => {
const { stdout, exitCode } = await runRepl("const x = 42\nx * 2\n.exit\n");
expect(stdout).toContain("84");
expect(exitCode).toBe(0);
});
test("handles function definition and call", async () => {
const { stdout, exitCode } = await runRepl("function add(a, b) { return a + b; }\nadd(2, 3)\n.exit\n");
expect(stdout).toContain("5");
expect(exitCode).toBe(0);
});
test("handles syntax errors gracefully", async () => {
const { stdout, stderr, exitCode } = await runRepl("const x =\n.exit\n");
// Should show error but continue
const output = stdout + stderr;
expect(output.toLowerCase()).toMatch(/error|parse|syntax/i);
expect(exitCode).toBe(0);
});
test("handles runtime errors gracefully", async () => {
const { stdout, stderr, exitCode } = await runRepl("throw new Error('test error')\n.exit\n");
// Should show error but continue
const output = stdout + stderr;
expect(output).toContain("Error");
expect(output).toContain("test error");
expect(exitCode).toBe(0);
});
test("handles undefined variable error", async () => {
const { stdout, stderr, exitCode } = await runRepl("undefinedVariable\n.exit\n");
// Should show reference error
const output = stdout + stderr;
expect(output.toLowerCase()).toMatch(/error|not defined|undefined/i);
expect(exitCode).toBe(0);
});
test("handles async/await", async () => {
const { stdout, exitCode } = await runRepl("await Promise.resolve(42)\n.exit\n");
expect(stdout).toContain("42");
expect(exitCode).toBe(0);
});
test("Bun object is available", async () => {
const { stdout, exitCode } = await runRepl("typeof Bun\n.exit\n");
expect(stdout).toContain("object");
expect(exitCode).toBe(0);
});
test("Bun.version is available", async () => {
const { stdout, exitCode } = await runRepl("Bun.version\n.exit\n");
// Should contain version number pattern
expect(stdout).toMatch(/\d+\.\d+\.\d+/);
expect(exitCode).toBe(0);
});
test("process object is available", async () => {
const { stdout, exitCode } = await runRepl("typeof process\n.exit\n");
expect(stdout).toContain("object");
expect(exitCode).toBe(0);
});
test.todo("handles TypeScript syntax", async () => {
// TypeScript type annotation should be stripped
// Currently not supported in REPL
const { stdout, exitCode } = await runRepl("const x: number = 42; x\n.exit\n");
expect(stdout).toContain("42");
expect(exitCode).toBe(0);
});
test("handles arrow functions", async () => {
const { stdout, exitCode } = await runRepl("const double = (x) => x * 2; double(21)\n.exit\n");
expect(stdout).toContain("42");
expect(exitCode).toBe(0);
});
test("handles template literals", async () => {
const { stdout, exitCode } = await runRepl("const name = 'world'; `hello ${name}`\n.exit\n");
expect(stdout).toContain("hello world");
expect(exitCode).toBe(0);
});
test("handles destructuring", async () => {
const { stdout, exitCode } = await runRepl("const {a, b} = {a: 1, b: 2}; a + b\n.exit\n");
expect(stdout).toContain("3");
expect(exitCode).toBe(0);
});
test("handles spread operator", async () => {
const { stdout, exitCode } = await runRepl("const arr = [1, 2, 3]; [...arr, 4, 5]\n.exit\n");
expect(stdout).toContain("4");
expect(stdout).toContain("5");
expect(exitCode).toBe(0);
});
test("handles class definition", async () => {
const { stdout, exitCode } = await runRepl("class Foo { constructor(x) { this.x = x; } }; new Foo(42).x\n.exit\n");
expect(stdout).toContain("42");
expect(exitCode).toBe(0);
});
test("handles Map and Set", async () => {
const { stdout, exitCode } = await runRepl("const m = new Map(); m.set('a', 1); m.get('a')\n.exit\n");
expect(stdout).toContain("1");
expect(exitCode).toBe(0);
});
test("handles BigInt", async () => {
const { stdout, exitCode } = await runRepl("1n + 2n\n.exit\n");
expect(stdout).toContain("3n");
expect(exitCode).toBe(0);
});
test("handles Symbol", async () => {
const { stdout, exitCode } = await runRepl('const s = Symbol("test"); typeof s\n.exit\n');
expect(stdout).toContain("symbol");
expect(exitCode).toBe(0);
});
test("handles JSON operations", async () => {
const { stdout, exitCode } = await runRepl("JSON.parse('{\"a\":1}')\n.exit\n");
expect(stdout).toMatch(/a.*:.*1/);
expect(exitCode).toBe(0);
});
test("handles fetch API availability", async () => {
const { stdout, exitCode } = await runRepl("typeof fetch\n.exit\n");
expect(stdout).toContain("function");
expect(exitCode).toBe(0);
});
test("handles URL API", async () => {
const { stdout, exitCode } = await runRepl('new URL("https://bun.sh").hostname\n.exit\n');
expect(stdout).toContain("bun.sh");
expect(exitCode).toBe(0);
});
test("handles TextEncoder/TextDecoder", async () => {
const { stdout, exitCode } = await runRepl('new TextDecoder().decode(new TextEncoder().encode("hi"))\n.exit\n');
expect(stdout).toContain("hi");
expect(exitCode).toBe(0);
});
test("handles globalThis", async () => {
const { stdout, exitCode } = await runRepl("typeof globalThis\n.exit\n");
expect(stdout).toContain("object");
expect(exitCode).toBe(0);
});
test("null result shows null", async () => {
const { stdout, exitCode } = await runRepl("null\n.exit\n");
expect(stdout).toContain("null");
expect(exitCode).toBe(0);
});
test("boolean results", async () => {
const { stdout, exitCode } = await runRepl("true\nfalse\n.exit\n");
expect(stdout).toContain("true");
expect(stdout).toContain("false");
expect(exitCode).toBe(0);
});
test("handles Infinity and NaN", async () => {
const { stdout, exitCode } = await runRepl("Infinity\nNaN\n.exit\n");
expect(stdout).toContain("Infinity");
expect(stdout).toContain("NaN");
expect(exitCode).toBe(0);
});
test("handles regex", async () => {
const { stdout, exitCode } = await runRepl('/test/.test("testing")\n.exit\n');
expect(stdout).toContain("true");
expect(exitCode).toBe(0);
});
test("handles Date", async () => {
const { stdout, exitCode } = await runRepl("new Date(0).getFullYear()\n.exit\n");
expect(stdout).toContain("1970");
expect(exitCode).toBe(0);
});
test("handles Math functions", async () => {
const { stdout, exitCode } = await runRepl("Math.max(1, 2, 3)\n.exit\n");
expect(stdout).toContain("3");
expect(exitCode).toBe(0);
});
test("handles Object methods", async () => {
const { stdout, exitCode } = await runRepl("Object.keys({a: 1, b: 2})\n.exit\n");
expect(stdout).toContain("a");
expect(stdout).toContain("b");
expect(exitCode).toBe(0);
});
test("handles Array methods", async () => {
const { stdout, exitCode } = await runRepl("[1, 2, 3].map(x => x * 2)\n.exit\n");
expect(stdout).toContain("2");
expect(stdout).toContain("4");
expect(stdout).toContain("6");
expect(exitCode).toBe(0);
});
test("handles String methods", async () => {
const { stdout, exitCode } = await runRepl('"hello".toUpperCase()\n.exit\n');
expect(stdout).toContain("HELLO");
expect(exitCode).toBe(0);
});
});

View File

@@ -0,0 +1,291 @@
import { describe, expect, test } from "bun:test";
import vm from "node:vm";
describe("Bun.Transpiler replMode", () => {
describe("basic transform output", () => {
const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });
test("simple expression wrapped in value object", () => {
const result = transpiler.transformSync("42");
// Should contain value wrapper
expect(result).toContain("value:");
});
test("variable declaration with await", () => {
const result = transpiler.transformSync("var x = await 1");
// Should hoist var declaration
expect(result).toContain("var x");
// Should have async wrapper
expect(result).toContain("async");
});
test("const becomes var with await", () => {
const result = transpiler.transformSync("const x = await 1");
// const should become var for REPL persistence (becomes context property)
expect(result).toContain("var x");
expect(result).not.toContain("const x");
});
test("let becomes var with await", () => {
const result = transpiler.transformSync("let x = await 1");
// let should become var for REPL persistence (becomes context property)
expect(result).toContain("var x");
expect(result).not.toContain("let x");
expect(result).toContain("async");
});
test("no async wrapper when no await", () => {
const result = transpiler.transformSync("var x = 1; x + 5");
// Should still have value wrapper for the last expression
expect(result).toContain("value:");
// Should not wrap in async when no await
expect(result).not.toMatch(/\(\s*async\s*\(\s*\)\s*=>/);
});
test("function declaration with await", () => {
const result = transpiler.transformSync("await 1; function foo() { return 42; }");
// Should hoist function declaration
expect(result).toContain("var foo");
expect(result).toContain("async");
});
test("class declaration with await", () => {
const result = transpiler.transformSync("await 1; class Bar { }");
// Should hoist class declaration with var (not let) for vm context persistence
expect(result).toContain("var Bar");
expect(result).toContain("async");
});
});
describe("REPL session with node:vm", () => {
const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });
async function runRepl(code: string, context?: object) {
const ctx = vm.createContext(context ?? { console, Promise });
const transformed = transpiler.transformSync(code);
return await vm.runInContext(transformed, ctx);
}
test("simple expression returns value object", async () => {
const result = await runRepl("42");
expect(result).toEqual({ value: 42 });
});
test("arithmetic expression", async () => {
const result = await runRepl("2 + 3 * 4");
expect(result).toEqual({ value: 14 });
});
test("string expression", async () => {
const result = await runRepl('"hello world"');
expect(result).toEqual({ value: "hello world" });
});
test("object literal (auto-detected)", async () => {
// Object literals don't need parentheses - the transpiler auto-detects them
const result = await runRepl("{a: 1, b: 2}");
expect(result).toEqual({ value: { a: 1, b: 2 } });
});
test("array literal", async () => {
const result = await runRepl("[1, 2, 3]");
expect(result).toEqual({ value: [1, 2, 3] });
});
test("await expression", async () => {
const result = await runRepl("await Promise.resolve(100)");
expect(result).toEqual({ value: 100 });
});
test("await with variable", async () => {
const ctx = vm.createContext({ Promise });
const code1 = transpiler.transformSync("var x = await Promise.resolve(10)");
await vm.runInContext(code1, ctx);
expect(ctx.x).toBe(10);
const code2 = transpiler.transformSync("x * 2");
const result = await vm.runInContext(code2, ctx);
expect(result).toEqual({ value: 20 });
});
});
describe("variable persistence across lines", () => {
const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });
async function runReplSession(lines: string[]) {
const ctx = vm.createContext({ console, Promise });
const results: any[] = [];
for (const line of lines) {
const transformed = transpiler.transformSync(line);
const result = await vm.runInContext(transformed, ctx);
results.push(result?.value ?? result);
}
return { results, context: ctx };
}
test("var persists across lines", async () => {
const { results, context } = await runReplSession(["var x = 10", "x + 5", "x = 20", "x"]);
expect(results[1]).toBe(15);
expect(results[3]).toBe(20);
expect(context.x).toBe(20);
});
test("let persists with await", async () => {
const { results } = await runReplSession(["let y = await Promise.resolve(100)", "y * 2"]);
expect(results[1]).toBe(200);
});
test("function declarations persist", async () => {
const { results, context } = await runReplSession(["await 1; function add(a, b) { return a + b; }", "add(2, 3)"]);
expect(results[1]).toBe(5);
expect(typeof context.add).toBe("function");
});
test("class declarations persist to vm context", async () => {
// Class declarations use 'var' hoisting so they persist to vm context
const { results, context } = await runReplSession([
"await 1; class Counter { constructor() { this.count = 0; } inc() { this.count++; } }",
"new Counter()",
]);
// The class is returned in the result's value
expect(typeof results[0]).toBe("function");
expect(results[0].name).toBe("Counter");
// The class should be accessible in subsequent REPL lines
expect(results[1]).toBeInstanceOf(context.Counter);
expect(typeof context.Counter).toBe("function");
});
});
describe("object literal detection", () => {
const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });
async function runRepl(code: string, context?: object) {
const ctx = vm.createContext(context ?? { console, Promise });
const transformed = transpiler.transformSync(code);
return await vm.runInContext(transformed, ctx);
}
test("{a: 1} parsed as object literal, not block", async () => {
const result = await runRepl("{a: 1}");
expect(result.value).toEqual({ a: 1 });
});
test("{a: 1, b: 2} parsed as object literal", async () => {
const result = await runRepl("{a: 1, b: 2}");
expect(result.value).toEqual({ a: 1, b: 2 });
});
test("{foo: await bar()} parsed as object literal", async () => {
const ctx = vm.createContext({
bar: async () => 42,
});
const code = transpiler.transformSync("{foo: await bar()}");
const result = await vm.runInContext(code, ctx);
expect(result.value).toEqual({ foo: 42 });
});
test("{x: 1}; is NOT wrapped (has trailing semicolon)", async () => {
// With semicolon, it's explicitly a block statement
const code = transpiler.transformSync("{x: 1};");
// The output should NOT treat this as an object literal
// It should be a block with a labeled statement, no value wrapper
expect(code).not.toContain("value:");
expect(code).toContain("x:");
});
test("whitespace around object literal is handled", async () => {
const result = await runRepl(" { a: 1 } ");
expect(result.value).toEqual({ a: 1 });
});
});
describe("edge cases", () => {
const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });
test("empty input", () => {
const result = transpiler.transformSync("");
expect(result).toBe("");
});
test("whitespace only", () => {
const result = transpiler.transformSync(" \n\t ");
expect(result.trim()).toBe("");
});
test("comment only produces empty output", () => {
// Comments are stripped by the transpiler
const result = transpiler.transformSync("// just a comment");
expect(result.trim()).toBe("");
});
test("TypeScript types stripped", () => {
const result = transpiler.transformSync("const x: number = await Promise.resolve(42)");
expect(result).not.toContain(": number");
});
test("multiple await expressions", async () => {
const ctx = vm.createContext({ Promise });
const code = transpiler.transformSync("await 1; await 2; await 3");
const result = await vm.runInContext(code, ctx);
// Last expression should be wrapped
expect(result).toEqual({ value: 3 });
});
test("destructuring assignment persists", async () => {
const ctx = vm.createContext({ Promise });
const code = transpiler.transformSync("var { a, b } = await Promise.resolve({ a: 1, b: 2 })");
await vm.runInContext(code, ctx);
expect(ctx.a).toBe(1);
expect(ctx.b).toBe(2);
});
test("array destructuring persists", async () => {
const ctx = vm.createContext({ Promise });
const code = transpiler.transformSync("var [x, y, z] = await Promise.resolve([10, 20, 30])");
await vm.runInContext(code, ctx);
expect(ctx.x).toBe(10);
expect(ctx.y).toBe(20);
expect(ctx.z).toBe(30);
});
});
describe("no transform cases", () => {
const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });
test("async function expression - no async wrapper", () => {
const result = transpiler.transformSync("async function foo() { await 1; }");
// await inside async function doesn't trigger TLA transform
// The top level has no await
expect(result).not.toMatch(/^\(async/);
});
test("arrow async function - no async wrapper", () => {
const result = transpiler.transformSync("const fn = async () => await 1");
// await inside arrow function doesn't trigger TLA transform
expect(result).not.toMatch(/^\(async\s*\(\)/);
});
});
describe("replMode option", () => {
test("replMode false by default", () => {
const transpiler = new Bun.Transpiler({ loader: "tsx" });
const result = transpiler.transformSync("42");
// Without replMode, no value wrapper
expect(result).not.toContain("value:");
});
test("replMode true adds transforms", () => {
const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });
const result = transpiler.transformSync("42");
// With replMode, value wrapper should be present
expect(result).toContain("value:");
});
});
});