const std = @import("std");
const logger = bun.logger;
const Runtime = @import("runtime.zig").Runtime;
const bun = @import("bun");
const string = bun.string;
const Output = bun.Output;
const Environment = bun.Environment;
const strings = bun.strings;
const MutableString = bun.MutableString;
const stringZ = bun.stringZ;
const default_allocator = bun.default_allocator;

pub const Ref = @import("ast/base.zig").Ref;
pub const Index = @import("ast/base.zig").Index;
const RefHashCtx = @import("ast/base.zig").RefHashCtx;
const ImportRecord = @import("import_record.zig").ImportRecord;
const JSC = bun.JSC;
const JSONParser = bun.JSON;
const ComptimeStringMap = bun.ComptimeStringMap;
const JSPrinter = @import("./js_printer.zig");
const js_lexer = @import("./js_lexer.zig");
const TypeScript = @import("./js_parser.zig").TypeScript;
const MimeType = bun.http.MimeType;
const OOM = bun.OOM;
const Loader = bun.options.Loader;
/// This is the index to the automatically-generated part containing code that
/// calls "__export(exports, { ... getters ... })". This is used to generate
/// getters on an exports object for ES6 export statements, and is both for
/// ES6 star imports and CommonJS-style modules. All files have one of these,
/// although it may contain no statements if there is nothing to export.
pub const namespace_export_part_index = 0;

/// This "Store" is a specialized memory allocation strategy very similar to an
/// arena, used for allocating expression and statement nodes during JavaScript
/// parsing and visiting. Allocations are grouped into large blocks, where each
/// block is treated as a fixed-buffer allocator. When a block runs out of
/// space, a new one is created; all blocks are joined as a linked list.
///
/// Similarly to an arena, you can call .reset() to reset state, reusing memory
/// across operations.
pub fn NewStore(comptime types: []const type, comptime count: usize) type {
    const largest_size, const largest_align = brk: {
        var largest_size = 0;
        var largest_align = 1;
        for (types) |T| {
            if (@sizeOf(T) == 0) {
                @compileError("NewStore does not support 0 size type: " ++ @typeName(T));
            }
            largest_size = @max(@sizeOf(T), largest_size);
            largest_align = @max(@alignOf(T), largest_align);
        }
        break :brk .{ largest_size, largest_align };
    };

    const backing_allocator = bun.default_allocator;

    const log = Output.scoped(.Store, true);

    return struct {
        const Store = @This();

        current: *Block,
        debug_lock: std.debug.SafetyLock = .{},

        pub const Block = struct {
            pub const size = largest_size * count * 2;
            pub const Size = std.math.IntFittingRange(0, size + largest_size);

            buffer: [size]u8 align(largest_align) = undefined,
            bytes_used: Size = 0,
            next: ?*Block = null,

            pub fn tryAlloc(block: *Block, comptime T: type) ?*T {
                const start = std.mem.alignForward(usize, block.bytes_used, @alignOf(T));
                if (start + @sizeOf(T) > block.buffer.len) return null;
                defer block.bytes_used = @intCast(start + @sizeOf(T));

                // it's simpler to use @ptrCast, but as a sanity check, we also
                // try to compute the slice. Zig will report an out of bounds
                // panic if the null detection logic above is wrong
                if (Environment.isDebug) {
                    _ = block.buffer[block.bytes_used..][0..@sizeOf(T)];
                }

                return @alignCast(@ptrCast(&block.buffer[start]));
            }
        };

        const PreAlloc = struct {
            metadata: Store,
            first_block: Block,
        };

        pub fn firstBlock(store: *Store) *Block {
            return &@as(*PreAlloc, @fieldParentPtr("metadata", store)).first_block;
        }

        pub fn init() *Store {
            log("init", .{});
            const prealloc = backing_allocator.create(PreAlloc) catch bun.outOfMemory();

            prealloc.first_block.bytes_used = 0;
            prealloc.first_block.next = null;

            prealloc.metadata = .{
                .current = &prealloc.first_block,
            };

            return &prealloc.metadata;
        }

        pub fn deinit(store: *Store) void {
            log("deinit", .{});
            var it = store.firstBlock().next; // do not free `store.head`
            while (it) |next| {
                if (Environment.isDebug or Environment.enable_asan)
                    @memset(next.buffer, undefined);
                it = next.next;
                backing_allocator.destroy(next);
            }

            const prealloc: PreAlloc = @fieldParentPtr("metadata", store);
            bun.assert(&prealloc.first_block == store.head);
            backing_allocator.destroy(prealloc);
        }

        pub fn reset(store: *Store) void {
            log("reset", .{});

            if (Environment.isDebug or Environment.enable_asan) {
                var it: ?*Block = store.firstBlock();
                while (it) |next| : (it = next.next) {
                    next.bytes_used = undefined;
                    @memset(&next.buffer, undefined);
                }
            }

            store.current = store.firstBlock();
            store.current.bytes_used = 0;
        }

        fn allocate(store: *Store, comptime T: type) *T {
            comptime bun.assert(@sizeOf(T) > 0); // don't allocate!
            comptime if (!supportsType(T)) {
                @compileError("Store does not know about type: " ++ @typeName(T));
            };

            if (store.current.tryAlloc(T)) |ptr|
                return ptr;

            // a new block is needed
            const next_block = if (store.current.next) |next| brk: {
                next.bytes_used = 0;
                break :brk next;
            } else brk: {
                const new_block = backing_allocator.create(Block) catch
                    bun.outOfMemory();
                new_block.next = null;
                new_block.bytes_used = 0;
                store.current.next = new_block;
                break :brk new_block;
            };

            store.current = next_block;

            return next_block.tryAlloc(T) orelse
                unreachable; // newly initialized blocks must have enough space for at least one
        }

        pub inline fn append(store: *Store, comptime T: type, data: T) *T {
            const ptr = store.allocate(T);
            if (Environment.isDebug) {
                log("append({s}) -> 0x{x}", .{ bun.meta.typeName(T), @intFromPtr(ptr) });
            }
            ptr.* = data;
            return ptr;
        }

        pub fn lock(store: *Store) void {
            store.debug_lock.lock();
        }

        pub fn unlock(store: *Store) void {
            store.debug_lock.unlock();
        }

        fn supportsType(T: type) bool {
            return std.mem.indexOfScalar(type, types, T) != null;
        }
    };
}

// There are three types.
// 1. Expr (expression)
// 2. Stmt (statement)
// 3. Binding
// Q: "What's the difference between an expression and a statement?"
// A:  > Expression: Something which evaluates to a value. Example: 1+2/x
//     > Statement: A line of code which does something. Example: GOTO 100
//     > https://stackoverflow.com/questions/19132/expression-versus-statement/19224#19224

// Expr, Binding, and Stmt each wrap a Data:
// Data is where the actual data where the node lives.
// There are four possible versions of this structure:
// [ ] 1.  *Expr, *Stmt, *Binding
// [ ] 1a. *Expr, *Stmt, *Binding something something dynamic dispatch
// [ ] 2.  *Data
// [x] 3.  Data.(*) (The union value in Data is a pointer)
// I chose #3 mostly for code simplification -- sometimes, the data is modified in-place.
// But also it uses the least memory.
// Since Data is a union, the size in bytes of Data is the max of all types
// So with #1 or #2, if S.Function consumes 768 bits, that means Data must be >= 768 bits
// Which means "true" in code now takes up over 768 bits, probably more than what v8 spends
// Instead, this approach means Data is the size of a pointer.
// It's not really clear which approach is best without benchmarking it.
// The downside with this approach is potentially worse memory locality, since the data for the node is somewhere else.
// But it could also be better memory locality due to smaller in-memory size (more likely to hit the cache)
// only benchmarks will provide an answer!
// But we must have pointers somewhere in here because can't have types that contain themselves
pub const BindingNodeIndex = Binding;
pub const StmtNodeIndex = Stmt;
pub const ExprNodeIndex = Expr;
pub const BabyList = bun.BabyList;

/// Slice that stores capacity and length in the same space as a regular slice.
pub const ExprNodeList = BabyList(Expr);

pub const StmtNodeList = []Stmt;
pub const BindingNodeList = []Binding;

pub const ImportItemStatus = enum(u2) {
    none,
    /// The linker doesn't report import/export mismatch errors
    generated,
    /// The printer will replace this import with "undefined"
    missing,

    pub fn jsonStringify(self: @This(), writer: anytype) !void {
        return try writer.write(@tagName(self));
    }
};

pub const AssignTarget = enum(u2) {
    none = 0,
    replace = 1, // "a = b"
    update = 2, // "a += b"
    pub fn jsonStringify(self: *const @This(), writer: anytype) !void {
        return try writer.write(@tagName(self));
    }
};

pub const LocRef = struct {
    loc: logger.Loc = logger.Loc.Empty,

    // TODO: remove this optional and make Ref a function getter
    // That will make this struct 128 bits instead of 192 bits and we can remove some heap allocations
    ref: ?Ref = null,
};

pub const Flags = struct {
    pub const JSXElement = enum {
        is_key_after_spread,
        has_any_dynamic,
        pub const Bitset = std.enums.EnumSet(JSXElement);
    };

    pub const Property = enum {
        is_computed,
        is_method,
        is_static,
        was_shorthand,
        is_spread,

        pub inline fn init(fields: Fields) Set {
            return Set.init(fields);
        }

        pub const None = Set{};
        pub const Fields = std.enums.EnumFieldStruct(Flags.Property, bool, false);
        pub const Set = std.enums.EnumSet(Flags.Property);
    };

    pub const Function = enum {
        is_async,
        is_generator,
        has_rest_arg,
        has_if_scope,

        is_forward_declaration,

        /// This is true if the function is a method
        is_unique_formal_parameters,

        /// Only applicable to function statements.
        is_export,

        pub inline fn init(fields: Fields) Set {
            return Set.init(fields);
        }

        pub const None = Set{};
        pub const Fields = std.enums.EnumFieldStruct(Function, bool, false);
        pub const Set = std.enums.EnumSet(Function);
    };
};

pub const Binding = struct {
    loc: logger.Loc,
    data: B,

    const Serializable = struct {
        type: Tag,
        object: string,
        value: B,
        loc: logger.Loc,
    };

    pub fn jsonStringify(self: *const @This(), writer: anytype) !void {
        return try writer.write(Serializable{ .type = std.meta.activeTag(self.data), .object = "binding", .value = self.data, .loc = self.loc });
    }

    pub fn ToExpr(comptime expr_type: type, comptime func_type: anytype) type {
        const ExprType = expr_type;
        return struct {
            context: *ExprType,
            allocator: std.mem.Allocator,
            pub const Context = @This();

            pub fn wrapIdentifier(ctx: *const Context, loc: logger.Loc, ref: Ref) Expr {
                return func_type(ctx.context, loc, ref);
            }

            pub fn init(context: *ExprType) Context {
                return Context{ .context = context, .allocator = context.allocator };
            }
        };
    }

    pub fn toExpr(binding: *const Binding, wrapper: anytype) Expr {
        const loc = binding.loc;

        switch (binding.data) {
            .b_missing => {
                return Expr{ .data = .{ .e_missing = E.Missing{} }, .loc = loc };
            },
            .b_identifier => |b| {
                return wrapper.wrapIdentifier(loc, b.ref);
            },
            .b_array => |b| {
                var exprs = wrapper.allocator.alloc(Expr, b.items.len) catch unreachable;
                var i: usize = 0;
                while (i < exprs.len) : (i += 1) {
                    const item = b.items[i];
                    exprs[i] = convert: {
                        const expr = toExpr(&item.binding, wrapper);
                        if (b.has_spread and i == exprs.len - 1) {
                            break :convert Expr.init(E.Spread, E.Spread{ .value = expr }, expr.loc);
                        } else if (item.default_value) |default| {
                            break :convert Expr.assign(expr, default);
                        } else {
                            break :convert expr;
                        }
                    };
                }

                return Expr.init(E.Array, E.Array{ .items = ExprNodeList.init(exprs), .is_single_line = b.is_single_line }, loc);
            },
            .b_object => |b| {
                const properties = wrapper
                    .allocator
                    .alloc(G.Property, b.properties.len) catch unreachable;
                for (properties, b.properties) |*property, item| {
                    property.* = .{
                        .flags = item.flags,
                        .key = item.key,
                        .kind = if (item.flags.contains(.is_spread))
                            .spread
                        else
                            .normal,
                        .value = toExpr(&item.value, wrapper),
                        .initializer = item.default_value,
                    };
                }
                return Expr.init(
                    E.Object,
                    E.Object{
                        .properties = G.Property.List.init(properties),
                        .is_single_line = b.is_single_line,
                    },
                    loc,
                );
            },
        }
    }

    pub const Tag = enum(u5) {
        b_identifier,
        b_array,
        b_object,
        b_missing,

        pub fn jsonStringify(self: @This(), writer: anytype) !void {
            return try writer.write(@tagName(self));
        }
    };

    pub var icount: usize = 0;

    pub fn init(t: anytype, loc: logger.Loc) Binding {
        icount += 1;
        switch (@TypeOf(t)) {
            *B.Identifier => {
                return Binding{ .loc = loc, .data = B{ .b_identifier = t } };
            },
            *B.Array => {
                return Binding{ .loc = loc, .data = B{ .b_array = t } };
            },
            *B.Object => {
                return Binding{ .loc = loc, .data = B{ .b_object = t } };
            },
            B.Missing => {
                return Binding{ .loc = loc, .data = B{ .b_missing = t } };
            },
            else => {
                @compileError("Invalid type passed to Binding.init");
            },
        }
    }

    pub fn alloc(allocator: std.mem.Allocator, t: anytype, loc: logger.Loc) Binding {
        icount += 1;
        switch (@TypeOf(t)) {
            B.Identifier => {
                const data = allocator.create(B.Identifier) catch unreachable;
                data.* = t;
                return Binding{ .loc = loc, .data = B{ .b_identifier = data } };
            },
            B.Array => {
                const data = allocator.create(B.Array) catch unreachable;
                data.* = t;
                return Binding{ .loc = loc, .data = B{ .b_array = data } };
            },
            B.Object => {
                const data = allocator.create(B.Object) catch unreachable;
                data.* = t;
                return Binding{ .loc = loc, .data = B{ .b_object = data } };
            },
            B.Missing => {
                return Binding{ .loc = loc, .data = B{ .b_missing = .{} } };
            },
            else => {
                @compileError("Invalid type passed to Binding.alloc");
            },
        }
    }
};

/// B is for Binding! Bindings are on the left side of variable
/// declarations (s_local), which is how destructuring assignments
/// are represented in memory. Consider a basic example.
///
///     let hello = world;
///         ^       ^
///         |       E.Identifier
///         B.Identifier
///
/// Bindings can be nested
///
///                B.Array
///                | B.Identifier
///                | |
///     let { foo: [ bar ] } = ...
///         ----------------
///         B.Object
pub const B = union(Binding.Tag) {
    // let x = ...
    b_identifier: *B.Identifier,
    // let [a, b] = ...
    b_array: *B.Array,
    // let { a, b: c } = ...
    b_object: *B.Object,
    // this is used to represent array holes
    b_missing: B.Missing,

    pub const Identifier = struct {
        ref: Ref,
    };

    pub const Property = struct {
        flags: Flags.Property.Set = Flags.Property.None,
        key: ExprNodeIndex,
        value: Binding,
        default_value: ?Expr = null,
    };

    pub const Object = struct {
        properties: []B.Property,
        is_single_line: bool = false,

        pub const Property = B.Property;
    };

    pub const Array = struct {
        items: []ArrayBinding,
        has_spread: bool = false,
        is_single_line: bool = false,

        pub const Item = ArrayBinding;
    };

    pub const Missing = struct {};

    /// This hash function is currently only used for React Fast Refresh transform.
    /// This doesn't include the `is_single_line` properties, as they only affect whitespace.
    pub fn writeToHasher(b: B, hasher: anytype, symbol_table: anytype) void {
        switch (b) {
            .b_identifier => |id| {
                const original_name = id.ref.getSymbol(symbol_table).original_name;
                writeAnyToHasher(hasher, .{ std.meta.activeTag(b), original_name.len });
            },
            .b_array => |array| {
                writeAnyToHasher(hasher, .{ std.meta.activeTag(b), array.has_spread, array.items.len });
                for (array.items) |item| {
                    writeAnyToHasher(hasher, .{item.default_value != null});
                    if (item.default_value) |default| {
                        default.data.writeToHasher(hasher, symbol_table);
                    }
                    item.binding.data.writeToHasher(hasher, symbol_table);
                }
            },
            .b_object => |object| {
                writeAnyToHasher(hasher, .{ std.meta.activeTag(b), object.properties.len });
                for (object.properties) |property| {
                    writeAnyToHasher(hasher, .{ property.default_value != null, property.flags });
                    if (property.default_value) |default| {
                        default.data.writeToHasher(hasher, symbol_table);
                    }
                    property.key.data.writeToHasher(hasher, symbol_table);
                    property.value.data.writeToHasher(hasher, symbol_table);
                }
            },
            .b_missing => {},
        }
    }
};

pub const ClauseItem = struct {
    alias: string,
    alias_loc: logger.Loc = logger.Loc.Empty,
    name: LocRef,

    /// This is the original name of the symbol stored in "Name". It's needed for
    /// "SExportClause" statements such as this:
    ///
    ///   export {foo as bar} from 'path'
    ///
    /// In this case both "foo" and "bar" are aliases because it's a re-export.
    /// We need to preserve both aliases in case the symbol is renamed. In this
    /// example, "foo" is "OriginalName" and "bar" is "Alias".
    original_name: string = "",

    pub const default_alias: string = "default";
};

pub const SlotCounts = struct {
    slots: Symbol.SlotNamespace.CountsArray = Symbol.SlotNamespace.CountsArray.initFill(0),

    pub fn unionMax(this: *SlotCounts, other: SlotCounts) void {
        for (&this.slots.values, other.slots.values) |*a, b| {
            if (a.* < b) a.* = b;
        }
    }
};

const char_freq_count = 64;
pub const CharAndCount = struct {
    char: u8 = 0,
    count: i32 = 0,
    index: usize = 0,

    pub const Array = [char_freq_count]CharAndCount;

    pub fn lessThan(_: void, a: CharAndCount, b: CharAndCount) bool {
        if (a.count != b.count) {
            return a.count > b.count;
        }

        if (a.index != b.index) {
            return a.index < b.index;
        }

        return a.char < b.char;
    }
};

pub const CharFreq = struct {
    const Vector = @Vector(char_freq_count, i32);
    const Buffer = [char_freq_count]i32;

    freqs: Buffer align(1) = undefined,

    const scan_big_chunk_size = 32;
    pub fn scan(this: *CharFreq, text: string, delta: i32) void {
        if (delta == 0)
            return;

        if (text.len < scan_big_chunk_size) {
            scanSmall(&this.freqs, text, delta);
        } else {
            scanBig(&this.freqs, text, delta);
        }
    }

    fn scanBig(out: *align(1) Buffer, text: string, delta: i32) void {
        // https://zig.godbolt.org/z/P5dPojWGK
        var freqs = out.*;
        defer out.* = freqs;
        var deltas: [256]i32 = [_]i32{0} ** 256;
        var remain = text;

        bun.assert(remain.len >= scan_big_chunk_size);

        const unrolled = remain.len - (remain.len % scan_big_chunk_size);
        const remain_end = remain.ptr + unrolled;
        var unrolled_ptr = remain.ptr;
        remain = remain[unrolled..];

        while (unrolled_ptr != remain_end) : (unrolled_ptr += scan_big_chunk_size) {
            const chunk = unrolled_ptr[0..scan_big_chunk_size].*;
            inline for (0..scan_big_chunk_size) |i| {
                deltas[@as(usize, chunk[i])] += delta;
            }
        }

        for (remain) |c| {
            deltas[@as(usize, c)] += delta;
        }

        freqs[0..26].* = deltas['a' .. 'a' + 26].*;
        freqs[26 .. 26 * 2].* = deltas['A' .. 'A' + 26].*;
        freqs[26 * 2 .. 62].* = deltas['0' .. '0' + 10].*;
        freqs[62] = deltas['_'];
        freqs[63] = deltas['$'];
    }

    fn scanSmall(out: *align(1) Buffer, text: string, delta: i32) void {
        var freqs: [char_freq_count]i32 = out.*;
        defer out.* = freqs;

        for (text) |c| {
            const i: usize = switch (c) {
                'a'...'z' => @as(usize, @intCast(c)) - 'a',
                'A'...'Z' => @as(usize, @intCast(c)) - ('A' - 26),
                '0'...'9' => @as(usize, @intCast(c)) + (53 - '0'),
                '_' => 62,
                '$' => 63,
                else => continue,
            };
            freqs[i] += delta;
        }
    }

    pub fn include(this: *CharFreq, other: CharFreq) void {
        // https://zig.godbolt.org/z/Mq8eK6K9s
        const left: @Vector(char_freq_count, i32) = this.freqs;
        const right: @Vector(char_freq_count, i32) = other.freqs;

        this.freqs = left + right;
    }

    pub fn compile(this: *const CharFreq, allocator: std.mem.Allocator) NameMinifier {
        const array: CharAndCount.Array = brk: {
            var _array: CharAndCount.Array = undefined;

            for (&_array, NameMinifier.default_tail, this.freqs, 0..) |*dest, char, freq, i| {
                dest.* = CharAndCount{
                    .char = char,
                    .index = i,
                    .count = freq,
                };
            }

            std.sort.pdq(CharAndCount, &_array, {}, CharAndCount.lessThan);

            break :brk _array;
        };

        var minifier = NameMinifier.init(allocator);
        minifier.head.ensureTotalCapacityPrecise(NameMinifier.default_head.len) catch unreachable;
        minifier.tail.ensureTotalCapacityPrecise(NameMinifier.default_tail.len) catch unreachable;
        // TODO: investigate counting number of < 0 and > 0 and pre-allocating
        for (array) |item| {
            if (item.char < '0' or item.char > '9') {
                minifier.head.append(item.char) catch unreachable;
            }
            minifier.tail.append(item.char) catch unreachable;
        }

        return minifier;
    }
};

pub const NameMinifier = struct {
    head: std.ArrayList(u8),
    tail: std.ArrayList(u8),

    pub const default_head = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$";
    pub const default_tail = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$";

    pub fn init(allocator: std.mem.Allocator) NameMinifier {
        return .{
            .head = std.ArrayList(u8).init(allocator),
            .tail = std.ArrayList(u8).init(allocator),
        };
    }

    pub fn numberToMinifiedName(this: *NameMinifier, name: *std.ArrayList(u8), _i: isize) !void {
        name.clearRetainingCapacity();
        var i = _i;
        var j = @as(usize, @intCast(@mod(i, 54)));
        try name.appendSlice(this.head.items[j .. j + 1]);
        i = @divFloor(i, 54);

        while (i > 0) {
            i -= 1;
            j = @as(usize, @intCast(@mod(i, char_freq_count)));
            try name.appendSlice(this.tail.items[j .. j + 1]);
            i = @divFloor(i, char_freq_count);
        }
    }

    pub fn defaultNumberToMinifiedName(allocator: std.mem.Allocator, _i: isize) !string {
        var i = _i;
        var j = @as(usize, @intCast(@mod(i, 54)));
        var name = std.ArrayList(u8).init(allocator);
        try name.appendSlice(default_head[j .. j + 1]);
        i = @divFloor(i, 54);

        while (i > 0) {
            i -= 1;
            j = @as(usize, @intCast(@mod(i, char_freq_count)));
            try name.appendSlice(default_tail[j .. j + 1]);
            i = @divFloor(i, char_freq_count);
        }

        return name.items;
    }
};

pub const G = struct {
    pub const Decl = struct {
        binding: BindingNodeIndex,
        value: ?ExprNodeIndex = null,

        pub const List = BabyList(Decl);
    };

    pub const NamespaceAlias = struct {
        namespace_ref: Ref,
        alias: string,

        was_originally_property_access: bool = false,

        import_record_index: u32 = std.math.maxInt(u32),
    };

    pub const ExportStarAlias = struct {
        loc: logger.Loc,

        // Although this alias name starts off as being the same as the statement's
        // namespace symbol, it may diverge if the namespace symbol name is minified.
        // The original alias name is preserved here to avoid this scenario.
        original_name: string,
    };

    pub const Class = struct {
        class_keyword: logger.Range = logger.Range.None,
        ts_decorators: ExprNodeList = ExprNodeList{},
        class_name: ?LocRef = null,
        extends: ?ExprNodeIndex = null,
        body_loc: logger.Loc = logger.Loc.Empty,
        close_brace_loc: logger.Loc = logger.Loc.Empty,
        properties: []Property = &([_]Property{}),
        has_decorators: bool = false,

        pub fn canBeMoved(this: *const Class) bool {
            if (this.extends != null)
                return false;

            if (this.has_decorators) {
                return false;
            }

            for (this.properties) |property| {
                if (property.kind == .class_static_block)
                    return false;

                const flags = property.flags;
                if (flags.contains(.is_computed) or flags.contains(.is_spread)) {
                    return false;
                }

                if (property.kind == .normal) {
                    if (flags.contains(.is_static)) {
                        for ([2]?Expr{ property.value, property.initializer }) |val_| {
                            if (val_) |val| {
                                switch (val.data) {
                                    .e_arrow, .e_function => {},
                                    else => {
                                        if (!val.canBeMoved()) {
                                            return false;
                                        }
                                    },
                                }
                            }
                        }
                    }
                }
            }

            return true;
        }
    };

    // invalid shadowing if left as Comment
    pub const Comment = struct { loc: logger.Loc, text: string };

    pub const ClassStaticBlock = struct {
        stmts: BabyList(Stmt) = .{},
        loc: logger.Loc,
    };

    pub const Property = struct {
        /// This is used when parsing a pattern that uses default values:
        ///
        ///   [a = 1] = [];
        ///   ({a = 1} = {});
        ///
        /// It's also used for class fields:
        ///
        ///   class Foo { a = 1 }
        ///
        initializer: ?ExprNodeIndex = null,
        kind: Kind = .normal,
        flags: Flags.Property.Set = Flags.Property.None,

        class_static_block: ?*ClassStaticBlock = null,
        ts_decorators: ExprNodeList = .{},
        // Key is optional for spread
        key: ?ExprNodeIndex = null,

        // This is omitted for class fields
        value: ?ExprNodeIndex = null,

        ts_metadata: TypeScript.Metadata = .m_none,

        pub const List = BabyList(Property);

        pub fn deepClone(this: *const Property, allocator: std.mem.Allocator) !Property {
            var class_static_block: ?*ClassStaticBlock = null;
            if (this.class_static_block != null) {
                class_static_block = bun.create(allocator, ClassStaticBlock, .{
                    .loc = this.class_static_block.?.loc,
                    .stmts = try this.class_static_block.?.stmts.clone(allocator),
                });
            }
            return .{
                .initializer = if (this.initializer) |init| try init.deepClone(allocator) else null,
                .kind = this.kind,
                .flags = this.flags,
                .class_static_block = class_static_block,
                .ts_decorators = try this.ts_decorators.deepClone(allocator),
                .key = if (this.key) |key| try key.deepClone(allocator) else null,
                .value = if (this.value) |value| try value.deepClone(allocator) else null,
                .ts_metadata = this.ts_metadata,
            };
        }

        pub const Kind = enum(u3) {
            normal,
            get,
            set,
            spread,
            declare,
            abstract,
            class_static_block,

            pub fn jsonStringify(self: @This(), writer: anytype) !void {
                return try writer.write(@tagName(self));
            }
        };
    };

    pub const FnBody = struct {
        loc: logger.Loc,
        stmts: StmtNodeList,

        pub fn initReturnExpr(allocator: std.mem.Allocator, expr: Expr) !FnBody {
            return .{
                .stmts = try allocator.dupe(Stmt, &.{Stmt.alloc(S.Return, .{
                    .value = expr,
                }, expr.loc)}),
                .loc = expr.loc,
            };
        }
    };

    pub const Fn = struct {
        name: ?LocRef = null,
        open_parens_loc: logger.Loc = logger.Loc.Empty,
        args: []Arg = &.{},
        // This was originally nullable, but doing so I believe caused a miscompilation
        // Specifically, the body was always null.
        body: FnBody = .{ .loc = logger.Loc.Empty, .stmts = &.{} },
        arguments_ref: ?Ref = null,

        flags: Flags.Function.Set = Flags.Function.None,

        return_ts_metadata: TypeScript.Metadata = .m_none,

        pub fn deepClone(this: *const Fn, allocator: std.mem.Allocator) !Fn {
            const args = try allocator.alloc(Arg, this.args.len);
            for (0..args.len) |i| {
                args[i] = try this.args[i].deepClone(allocator);
            }
            return .{
                .name = this.name,
                .open_parens_loc = this.open_parens_loc,
                .args = args,
                .body = .{
                    .loc = this.body.loc,
                    .stmts = this.body.stmts,
                },
                .arguments_ref = this.arguments_ref,
                .flags = this.flags,
                .return_ts_metadata = this.return_ts_metadata,
            };
        }
    };
    pub const Arg = struct {
        ts_decorators: ExprNodeList = ExprNodeList{},
        binding: BindingNodeIndex,
        default: ?ExprNodeIndex = null,

        // "constructor(public x: boolean) {}"
        is_typescript_ctor_field: bool = false,

        ts_metadata: TypeScript.Metadata = .m_none,

        pub fn deepClone(this: *const Arg, allocator: std.mem.Allocator) !Arg {
            return .{
                .ts_decorators = try this.ts_decorators.deepClone(allocator),
                .binding = this.binding,
                .default = if (this.default) |d| try d.deepClone(allocator) else null,
                .is_typescript_ctor_field = this.is_typescript_ctor_field,
                .ts_metadata = this.ts_metadata,
            };
        }
    };
};

pub const Symbol = struct {
    /// This is the name that came from the parser. Printed names may be renamed
    /// during minification or to avoid name collisions. Do not use the original
    /// name during printing.
    original_name: []const u8,

    /// This is used for symbols that represent items in the import clause of an
    /// ES6 import statement. These should always be referenced by EImportIdentifier
    /// instead of an EIdentifier. When this is present, the expression should
    /// be printed as a property access off the namespace instead of as a bare
    /// identifier.
    ///
    /// For correctness, this must be stored on the symbol instead of indirectly
    /// associated with the Ref for the symbol somehow. In ES6 "flat bundling"
    /// mode, re-exported symbols are collapsed using MergeSymbols() and renamed
    /// symbols from other files that end up at this symbol must be able to tell
    /// if it has a namespace alias.
    namespace_alias: ?G.NamespaceAlias = null,

    /// Used by the parser for single pass parsing.
    link: Ref = Ref.None,

    /// An estimate of the number of uses of this symbol. This is used to detect
    /// whether a symbol is used or not. For example, TypeScript imports that are
    /// unused must be removed because they are probably type-only imports. This
    /// is an estimate and may not be completely accurate due to oversights in the
    /// code. But it should always be non-zero when the symbol is used.
    use_count_estimate: u32 = 0,

    /// This is for generating cross-chunk imports and exports for code splitting.
    ///
    /// Do not use this directly. Use `chunkIndex()` instead.
    chunk_index: u32 = invalid_chunk_index,

    /// This is used for minification. Symbols that are declared in sibling scopes
    /// can share a name. A good heuristic (from Google Closure Compiler) is to
    /// assign names to symbols from sibling scopes in declaration order. That way
    /// local variable names are reused in each global function like this, which
    /// improves gzip compression:
    ///
    ///   function x(a, b) { ... }
    ///   function y(a, b, c) { ... }
    ///
    /// The parser fills this in for symbols inside nested scopes. There are three
    /// slot namespaces: regular symbols, label symbols, and private symbols.
    ///
    /// Do not use this directly. Use `nestedScopeSlot()` instead.
    nested_scope_slot: u32 = invalid_nested_scope_slot,

    did_keep_name: bool = true,

    must_start_with_capital_letter_for_jsx: bool = false,

    /// The kind of symbol. This is used to determine how to print the symbol
    /// and how to deal with conflicts, renaming, etc.
    kind: Kind = Kind.other,

    /// Certain symbols must not be renamed or minified. For example, the
    /// "arguments" variable is declared by the runtime for every function.
    /// Renaming can also break any identifier used inside a "with" statement.
    must_not_be_renamed: bool = false,

    /// We automatically generate import items for property accesses off of
    /// namespace imports. This lets us remove the expensive namespace imports
    /// while bundling in many cases, replacing them with a cheap import item
    /// instead:
    ///
    ///   import * as ns from 'path'
    ///   ns.foo()
    ///
    /// That can often be replaced by this, which avoids needing the namespace:
    ///
    ///   import {foo} from 'path'
    ///   foo()
    ///
    /// However, if the import is actually missing then we don't want to report a
    /// compile-time error like we do for real import items. This status lets us
    /// avoid this. We also need to be able to replace such import items with
    /// undefined, which this status is also used for.
    import_item_status: ImportItemStatus = ImportItemStatus.none,

    /// --- Not actually used yet -----------------------------------------------
    /// Sometimes we lower private symbols even if they are supported. For example,
    /// consider the following TypeScript code:
    ///
    ///   class Foo {
    ///     #foo = 123
    ///     bar = this.#foo
    ///   }
    ///
    /// If "useDefineForClassFields: false" is set in "tsconfig.json", then "bar"
    /// must use assignment semantics instead of define semantics. We can compile
    /// that to this code:
    ///
    ///   class Foo {
    ///     constructor() {
    ///       this.#foo = 123;
    ///       this.bar = this.#foo;
    ///     }
    ///     #foo;
    ///   }
    ///
    /// However, we can't do the same for static fields:
    ///
    ///   class Foo {
    ///     static #foo = 123
    ///     static bar = this.#foo
    ///   }
    ///
    /// Compiling these static fields to something like this would be invalid:
    ///
    ///   class Foo {
    ///     static #foo;
    ///   }
    ///   Foo.#foo = 123;
    ///   Foo.bar = Foo.#foo;
    ///
    /// Thus "#foo" must be lowered even though it's supported. Another case is
    /// when we're converting top-level class declarations to class expressions
    /// to avoid the TDZ and the class shadowing symbol is referenced within the
    /// class body:
    ///
    ///   class Foo {
    ///     static #foo = Foo
    ///   }
    ///
    /// This cannot be converted into something like this:
    ///
    ///   var Foo = class {
    ///     static #foo;
    ///   };
    ///   Foo.#foo = Foo;
    ///
    /// --- Not actually used yet -----------------------------------------------
    private_symbol_must_be_lowered: bool = false,

    remove_overwritten_function_declaration: bool = false,

    /// Used in HMR to decide when live binding code is needed.
    has_been_assigned_to: bool = false,

    comptime {
        bun.assert_eql(@sizeOf(Symbol), 88);
        bun.assert_eql(@alignOf(Symbol), @alignOf([]const u8));
    }

    const invalid_chunk_index = std.math.maxInt(u32);
    pub const invalid_nested_scope_slot = std.math.maxInt(u32);

    pub const SlotNamespace = enum {
        must_not_be_renamed,
        default,
        label,
        private_name,
        mangled_prop,

        pub const CountsArray = std.EnumArray(SlotNamespace, u32);
    };

    /// This is for generating cross-chunk imports and exports for code splitting.
    pub inline fn chunkIndex(this: *const Symbol) ?u32 {
        const i = this.chunk_index;
        return if (i == invalid_chunk_index) null else i;
    }

    pub inline fn nestedScopeSlot(this: *const Symbol) ?u32 {
        const i = this.nested_scope_slot;
        return if (i == invalid_nested_scope_slot) null else i;
    }

    pub fn slotNamespace(this: *const Symbol) SlotNamespace {
        const kind = this.kind;

        if (kind == .unbound or this.must_not_be_renamed) {
            return .must_not_be_renamed;
        }

        if (kind.isPrivate()) {
            return .private_name;
        }

        return switch (kind) {
            // .mangled_prop => .mangled_prop,
            .label => .label,
            else => .default,
        };
    }

    pub inline fn hasLink(this: *const Symbol) bool {
        return this.link.tag != .invalid;
    }

    pub const Kind = enum {
        /// An unbound symbol is one that isn't declared in the file it's referenced
        /// in. For example, using "window" without declaring it will be unbound.
        unbound,

        /// This has special merging behavior. You're allowed to re-declare these
        /// symbols more than once in the same scope. These symbols are also hoisted
        /// out of the scope they are declared in to the closest containing function
        /// or module scope. These are the symbols with this kind:
        ///
        /// - Function arguments
        /// - Function statements
        /// - Variables declared using "var"
        hoisted,
        hoisted_function,

        /// There's a weird special case where catch variables declared using a simple
        /// identifier (i.e. not a binding pattern) block hoisted variables instead of
        /// becoming an error:
        ///
        ///   var e = 0;
        ///   try { throw 1 } catch (e) {
        ///     print(e) // 1
        ///     var e = 2
        ///     print(e) // 2
        ///   }
        ///   print(e) // 0 (since the hoisting stops at the catch block boundary)
        ///
        /// However, other forms are still a syntax error:
        ///
        ///   try {} catch (e) { let e }
        ///   try {} catch ({e}) { var e }
        ///
        /// This symbol is for handling this weird special case.
        catch_identifier,

        /// Generator and async functions are not hoisted, but still have special
        /// properties such as being able to overwrite previous functions with the
        /// same name
        generator_or_async_function,

        /// This is the special "arguments" variable inside functions
        arguments,

        /// Classes can merge with TypeScript namespaces.
        class,

        /// A class-private identifier (i.e. "#foo").
        private_field,
        private_method,
        private_get,
        private_set,
        private_get_set_pair,
        private_static_field,
        private_static_method,
        private_static_get,
        private_static_set,
        private_static_get_set_pair,

        /// Labels are in their own namespace
        label,

        /// TypeScript enums can merge with TypeScript namespaces and other TypeScript
        /// enums.
        ts_enum,

        /// TypeScript namespaces can merge with classes, functions, TypeScript enums,
        /// and other TypeScript namespaces.
        ts_namespace,

        /// In TypeScript, imports are allowed to silently collide with symbols within
        /// the module. Presumably this is because the imports may be type-only.
        /// Import statement namespace references should NOT have this set.
        import,

        /// Assigning to a "const" symbol will throw a TypeError at runtime
        constant,

        // CSS identifiers that are renamed to be unique to the file they are in
        local_css,

        /// This annotates all other symbols that don't have special behavior.
        other,

        pub fn jsonStringify(self: @This(), writer: anytype) !void {
            return try writer.write(@tagName(self));
        }

        pub inline fn isPrivate(kind: Symbol.Kind) bool {
            return @intFromEnum(kind) >= @intFromEnum(Symbol.Kind.private_field) and @intFromEnum(kind) <= @intFromEnum(Symbol.Kind.private_static_get_set_pair);
        }

        pub inline fn isHoisted(kind: Symbol.Kind) bool {
            return switch (kind) {
                .hoisted, .hoisted_function => true,
                else => false,
            };
        }

        pub inline fn isHoistedOrFunction(kind: Symbol.Kind) bool {
            return switch (kind) {
                .hoisted, .hoisted_function, .generator_or_async_function => true,
                else => false,
            };
        }

        pub inline fn isFunction(kind: Symbol.Kind) bool {
            return switch (kind) {
                .hoisted_function, .generator_or_async_function => true,
                else => false,
            };
        }
    };

    pub const isKindPrivate = Symbol.Kind.isPrivate;
    pub const isKindHoisted = Symbol.Kind.isHoisted;
    pub const isKindHoistedOrFunction = Symbol.Kind.isHoistedOrFunction;
    pub const isKindFunction = Symbol.Kind.isFunction;

    pub const Use = struct {
        count_estimate: u32 = 0,
    };

    pub const List = BabyList(Symbol);
    pub const NestedList = BabyList(List);

    pub fn mergeContentsWith(this: *Symbol, old: *Symbol) void {
        this.use_count_estimate += old.use_count_estimate;
        if (old.must_not_be_renamed) {
            this.original_name = old.original_name;
            this.must_not_be_renamed = true;
        }

        // TODO: MustStartWithCapitalLetterForJSX
    }

    pub const Map = struct {
        // This could be represented as a "map[Ref]Symbol" but a two-level array was
        // more efficient in profiles. This appears to be because it doesn't involve
        // a hash. This representation also makes it trivial to quickly merge symbol
        // maps from multiple files together. Each file only generates symbols in a
        // single inner array, so you can join the maps together by just make a
        // single outer array containing all of the inner arrays. See the comment on
        // "Ref" for more detail.
        symbols_for_source: NestedList = .{},

        pub fn dump(this: Map) void {
            defer Output.flush();
            for (this.symbols_for_source.slice(), 0..) |symbols, i| {
                Output.prettyln("\n\n-- Source ID: {d} ({d} symbols) --\n\n", .{ i, symbols.len });
                for (symbols.slice(), 0..) |symbol, inner_index| {
                    Output.prettyln(
                        " name: {s}\n  tag: {s}\n       {any}\n",
                        .{
                            symbol.original_name, @tagName(symbol.kind),
                            if (symbol.hasLink()) symbol.link else Ref{
                                .source_index = @truncate(i),
                                .inner_index = @truncate(inner_index),
                                .tag = .symbol,
                            },
                        },
                    );
                }
            }
        }

        pub fn assignChunkIndex(this: *Map, decls_: DeclaredSymbol.List, chunk_index: u32) void {
            const Iterator = struct {
                map: *Map,
                chunk_index: u32,

                pub fn next(self: @This(), ref: Ref) void {
                    var symbol = self.map.get(ref).?;
                    symbol.chunk_index = self.chunk_index;
                }
            };
            var decls = decls_;

            DeclaredSymbol.forEachTopLevelSymbol(&decls, Iterator{ .map = this, .chunk_index = chunk_index }, Iterator.next);
        }

        pub fn merge(this: *Map, old: Ref, new: Ref) Ref {
            if (old.eql(new)) {
                return new;
            }

            var old_symbol = this.get(old).?;
            if (old_symbol.hasLink()) {
                const old_link = old_symbol.link;
                old_symbol.link = this.merge(old_link, new);
                return old_symbol.link;
            }

            var new_symbol = this.get(new).?;

            if (new_symbol.hasLink()) {
                const new_link = new_symbol.link;
                new_symbol.link = this.merge(old, new_link);
                return new_symbol.link;
            }

            old_symbol.link = new;
            new_symbol.mergeContentsWith(old_symbol);
            return new;
        }

        pub fn get(self: *const Map, ref: Ref) ?*Symbol {
            if (Ref.isSourceIndexNull(ref.sourceIndex()) or ref.isSourceContentsSlice()) {
                return null;
            }

            return self.symbols_for_source.at(ref.sourceIndex()).mut(ref.innerIndex());
        }

        pub fn getConst(self: *const Map, ref: Ref) ?*const Symbol {
            if (Ref.isSourceIndexNull(ref.sourceIndex()) or ref.isSourceContentsSlice()) {
                return null;
            }

            return self.symbols_for_source.at(ref.sourceIndex()).at(ref.innerIndex());
        }

        pub fn init(sourceCount: usize, allocator: std.mem.Allocator) !Map {
            const symbols_for_source: NestedList = NestedList.init(try allocator.alloc([]Symbol, sourceCount));
            return Map{ .symbols_for_source = symbols_for_source };
        }

        pub fn initWithOneList(list: List) Map {
            const baby_list = BabyList(List).init((&list)[0..1]);
            return initList(baby_list);
        }

        pub fn initList(list: NestedList) Map {
            return Map{ .symbols_for_source = list };
        }

        pub fn getWithLink(symbols: *const Map, ref: Ref) ?*Symbol {
            var symbol: *Symbol = symbols.get(ref) orelse return null;
            if (symbol.hasLink()) {
                return symbols.get(symbol.link) orelse symbol;
            }
            return symbol;
        }

        pub fn getWithLinkConst(symbols: *Map, ref: Ref) ?*const Symbol {
            var symbol: *const Symbol = symbols.getConst(ref) orelse return null;
            if (symbol.hasLink()) {
                return symbols.getConst(symbol.link) orelse symbol;
            }
            return symbol;
        }

        pub fn followAll(symbols: *Map) void {
            const trace = bun.perf.trace("Symbols.followAll");
            defer trace.end();
            for (symbols.symbols_for_source.slice()) |list| {
                for (list.slice()) |*symbol| {
                    if (!symbol.hasLink()) continue;
                    symbol.link = follow(symbols, symbol.link);
                }
            }
        }

        /// Equivalent to followSymbols in esbuild
        pub fn follow(symbols: *const Map, ref: Ref) Ref {
            var symbol = symbols.get(ref) orelse return ref;
            if (!symbol.hasLink()) {
                return ref;
            }

            const link = follow(symbols, symbol.link);

            if (!symbol.link.eql(link)) {
                symbol.link = link;
            }

            return link;
        }
    };

    pub inline fn isHoisted(self: *const Symbol) bool {
        return Symbol.isKindHoisted(self.kind);
    }
};

pub const OptionalChain = enum(u1) {
    /// "a?.b"
    start,

    /// "a?.b.c" => ".c" is .continuation
    /// "(a?.b).c" => ".c" is null
    continuation,

    pub fn jsonStringify(self: @This(), writer: anytype) !void {
        return try writer.write(@tagName(self));
    }
};

pub const E = struct {
    /// This represents an internal property name that can be mangled. The symbol
    /// referenced by this expression should be a "SymbolMangledProp" symbol.
    pub const NameOfSymbol = struct {
        ref: Ref = Ref.None,

        /// If true, a preceding comment contains "@__KEY__"
        ///
        /// Currently not used
        has_property_key_comment: bool = false,
    };

    pub const Array = struct {
        items: ExprNodeList = ExprNodeList{},
        comma_after_spread: ?logger.Loc = null,
        is_single_line: bool = false,
        is_parenthesized: bool = false,
        was_originally_macro: bool = false,
        close_bracket_loc: logger.Loc = logger.Loc.Empty,

        pub fn push(this: *Array, allocator: std.mem.Allocator, item: Expr) !void {
            try this.items.push(allocator, item);
        }

        pub inline fn slice(this: Array) []Expr {
            return this.items.slice();
        }

        pub fn inlineSpreadOfArrayLiterals(
            this: *Array,
            allocator: std.mem.Allocator,
            estimated_count: usize,
        ) !ExprNodeList {
            var out = try allocator.alloc(
                Expr,
                // This over-allocates a little but it's fine
                estimated_count + @as(usize, this.items.len),
            );
            var remain = out;
            for (this.items.slice()) |item| {
                switch (item.data) {
                    .e_spread => |val| {
                        if (val.value.data == .e_array) {
                            for (val.value.data.e_array.items.slice()) |inner_item| {
                                if (inner_item.data == .e_missing) {
                                    remain[0] = Expr.init(E.Undefined, .{}, inner_item.loc);
                                    remain = remain[1..];
                                } else {
                                    remain[0] = inner_item;
                                    remain = remain[1..];
                                }
                            }

                            // skip empty arrays
                            // don't include the inlined spread.
                            continue;
                        }
                        // non-arrays are kept in
                    },
                    else => {},
                }

                remain[0] = item;
                remain = remain[1..];
            }

            return ExprNodeList.init(out[0 .. out.len - remain.len]);
        }

        pub fn toJS(this: @This(), allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) ToJSError!JSC.JSValue {
            const items = this.items.slice();
            var array = JSC.JSValue.createEmptyArray(globalObject, items.len);
            array.protect();
            defer array.unprotect();
            for (items, 0..) |expr, j| {
                array.putIndex(globalObject, @as(u32, @truncate(j)), try expr.data.toJS(allocator, globalObject));
            }

            return array;
        }

        /// Assumes each item in the array is a string
        pub fn alphabetizeStrings(this: *Array) void {
            if (comptime Environment.allow_assert) {
                for (this.items.slice()) |item| {
                    bun.assert(item.data == .e_string);
                }
            }
            std.sort.pdq(Expr, this.items.slice(), {}, Sorter.isLessThan);
        }

        const Sorter = struct {
            pub fn isLessThan(ctx: void, lhs: Expr, rhs: Expr) bool {
                return strings.cmpStringsAsc(ctx, lhs.data.e_string.data, rhs.data.e_string.data);
            }
        };
    };

    pub const Unary = struct {
        op: Op.Code,
        value: ExprNodeIndex,
    };

    pub const Binary = struct {
        left: ExprNodeIndex,
        right: ExprNodeIndex,
        op: Op.Code,
    };

    pub const Boolean = struct {
        value: bool,
        pub fn toJS(this: @This(), ctx: *JSC.JSGlobalObject) JSC.C.JSValueRef {
            return JSC.C.JSValueMakeBoolean(ctx, this.value);
        }
    };
    pub const Super = struct {};
    pub const Null = struct {};
    pub const This = struct {};
    pub const Undefined = struct {};
    pub const New = struct {
        target: ExprNodeIndex,
        args: ExprNodeList = ExprNodeList{},

        // True if there is a comment containing "@__PURE__" or "#__PURE__" preceding
        // this call expression. See the comment inside ECall for more details.
        can_be_unwrapped_if_unused: bool = false,

        close_parens_loc: logger.Loc,
    };
    pub const NewTarget = struct {
        range: logger.Range,
    };
    pub const ImportMeta = struct {};
    pub const ImportMetaMain = struct {
        /// If we want to print `!import.meta.main`, set this flag to true
        /// instead of wrapping in a unary not. This way, the printer can easily
        /// print `require.main != module` instead of `!(require.main == module)`
        inverted: bool = false,
    };

    pub const Special = union(enum) {
        /// emits `exports` or `module.exports` depending on `commonjs_named_exports_deoptimized`
        module_exports,
        /// `import.meta.hot`
        hot_enabled,
        /// Acts as .e_undefined, but allows property accesses to the rest of the HMR API.
        hot_disabled,
        /// `import.meta.hot.data` when HMR is enabled. Not reachable when it is disabled.
        hot_data,
        /// `import.meta.hot.accept` when HMR is enabled. Truthy.
        hot_accept,
        /// Converted from `hot_accept` to this in js_parser.zig when it is
        /// passed strings. Printed as `hmr.hot.acceptSpecifiers`
        hot_accept_visited,
        /// Prints the resolved specifier string for an import record.
        resolved_specifier_string: ImportRecord.Index,
    };

    pub const Call = struct {
        // Node:
        target: ExprNodeIndex,
        args: ExprNodeList = ExprNodeList{},
        optional_chain: ?OptionalChain = null,
        is_direct_eval: bool = false,
        close_paren_loc: logger.Loc = logger.Loc.Empty,

        // True if there is a comment containing "@__PURE__" or "#__PURE__" preceding
        // this call expression. This is an annotation used for tree shaking, and
        // means that the call can be removed if it's unused. It does not mean the
        // call is pure (e.g. it may still return something different if called twice).
        //
        // Note that the arguments are not considered to be part of the call. If the
        // call itself is removed due to this annotation, the arguments must remain
        // if they have side effects.
        can_be_unwrapped_if_unused: bool = false,

        // Used when printing to generate the source prop on the fly
        was_jsx_element: bool = false,

        pub fn hasSameFlagsAs(a: *Call, b: *Call) bool {
            return (a.optional_chain == b.optional_chain and
                a.is_direct_eval == b.is_direct_eval and
                a.can_be_unwrapped_if_unused == b.can_be_unwrapped_if_unused);
        }
    };

    pub const Dot = struct {
        // target is Node
        target: ExprNodeIndex,
        name: string,
        name_loc: logger.Loc,
        optional_chain: ?OptionalChain = null,

        // If true, this property access is known to be free of side-effects. That
        // means it can be removed if the resulting value isn't used.
        can_be_removed_if_unused: bool = false,

        // If true, this property access is a function that, when called, can be
        // unwrapped if the resulting value is unused. Unwrapping means discarding
        // the call target but keeping any arguments with side effects.
        call_can_be_unwrapped_if_unused: bool = false,

        pub fn hasSameFlagsAs(a: *Dot, b: *Dot) bool {
            return (a.optional_chain == b.optional_chain and
                a.is_direct_eval == b.is_direct_eval and
                a.can_be_removed_if_unused == b.can_be_removed_if_unused and a.call_can_be_unwrapped_if_unused == b.call_can_be_unwrapped_if_unused);
        }
    };

    pub const Index = struct {
        index: ExprNodeIndex,
        target: ExprNodeIndex,
        optional_chain: ?OptionalChain = null,

        pub fn hasSameFlagsAs(a: *E.Index, b: *E.Index) bool {
            return (a.optional_chain == b.optional_chain);
        }
    };

    pub const Arrow = struct {
        args: []G.Arg = &[_]G.Arg{},
        body: G.FnBody,

        is_async: bool = false,
        has_rest_arg: bool = false,
        prefer_expr: bool = false, // Use shorthand if true and "Body" is a single return statement

        pub const noop_return_undefined: Arrow = .{
            .args = &.{},
            .body = .{
                .loc = .Empty,
                .stmts = &.{},
            },
        };
    };

    pub const Function = struct { func: G.Fn };

    pub const Identifier = struct {
        ref: Ref = Ref.None,

        // If we're inside a "with" statement, this identifier may be a property
        // access. In that case it would be incorrect to remove this identifier since
        // the property access may be a getter or setter with side effects.
        must_keep_due_to_with_stmt: bool = false,

        // If true, this identifier is known to not have a side effect (i.e. to not
        // throw an exception) when referenced. If false, this identifier may or
        // not have side effects when referenced. This is used to allow the removal
        // of known globals such as "Object" if they aren't used.
        can_be_removed_if_unused: bool = false,

        // If true, this identifier represents a function that, when called, can be
        // unwrapped if the resulting value is unused. Unwrapping means discarding
        // the call target but keeping any arguments with side effects.
        call_can_be_unwrapped_if_unused: bool = false,

        pub inline fn init(ref: Ref) Identifier {
            return Identifier{
                .ref = ref,
                .must_keep_due_to_with_stmt = false,
                .can_be_removed_if_unused = false,
                .call_can_be_unwrapped_if_unused = false,
            };
        }
    };

    /// This is similar to an `Identifier` but it represents a reference to an ES6
    /// import item.
    ///
    /// Depending on how the code is linked, the file containing this EImportIdentifier
    /// may or may not be in the same module group as the file it was imported from.
    ///
    /// If it's the same module group than we can just merge the import item symbol
    /// with the corresponding symbol that was imported, effectively renaming them
    /// to be the same thing and statically binding them together.
    ///
    /// But if it's a different module group, then the import must be dynamically
    /// evaluated using a property access off the corresponding namespace symbol,
    /// which represents the result of a require() call.
    ///
    /// It's stored as a separate type so it's not easy to confuse with a plain
    /// identifier. For example, it'd be bad if code trying to convert "{x: x}" into
    /// "{x}" shorthand syntax wasn't aware that the "x" in this case is actually
    /// "{x: importedNamespace.x}". This separate type forces code to opt-in to
    /// doing this instead of opt-out.
    pub const ImportIdentifier = struct {
        ref: Ref = Ref.None,

        /// If true, this was originally an identifier expression such as "foo". If
        /// false, this could potentially have been a member access expression such
        /// as "ns.foo" off of an imported namespace object.
        was_originally_identifier: bool = false,
    };

    /// This is a dot expression on exports, such as `exports.<ref>`. It is given
    /// it's own AST node to allow CommonJS unwrapping, in which this can just be
    /// the identifier in the Ref
    pub const CommonJSExportIdentifier = struct {
        ref: Ref = Ref.None,
        base: Base = .exports,

        /// The original variant of the dot expression must be known so that in the case that we
        /// - fail to convert this to ESM
        /// - ALSO see an assignment to `module.exports` (commonjs_module_exports_assigned_deoptimized)
        /// It must be known if `exports` or `module.exports` was written in source
        /// code, as the distinction will alter behavior. The fixup happens in the printer when
        /// printing this node.
        pub const Base = enum {
            exports,
            module_dot_exports,
        };
    };

    // This is similar to EIdentifier but it represents class-private fields and
    // methods. It can be used where computed properties can be used, such as
    // EIndex and Property.
    pub const PrivateIdentifier = struct {
        ref: Ref,
    };

    /// In development mode, the new JSX transform has a few special props
    /// - `React.jsxDEV(type, arguments, key, isStaticChildren, source, self)`
    /// - `arguments`:
    ///      ```{ ...props, children: children, }```
    /// - `source`: https://github.com/babel/babel/blob/ef87648f3f05ccc393f89dea7d4c7c57abf398ce/packages/babel-plugin-transform-react-jsx-source/src/index.js#L24-L48
    ///      ```{
    ///         fileName: string | null,
    ///         columnNumber: number | null,
    ///         lineNumber: number | null,
    ///      }```
    /// - `children`:
    ///     - static the function is React.jsxsDEV, "jsxs" instead of "jsx"
    ///     - one child? the function is React.jsxDEV,
    ///     - no children? the function is React.jsxDEV and children is an empty array.
    /// `isStaticChildren`: https://github.com/facebook/react/blob/4ca62cac45c288878d2532e5056981d177f9fdac/packages/react/src/jsx/ReactJSXElementValidator.js#L369-L384
    ///     This flag means children is an array of JSX Elements literals.
    ///     The documentation on this is sparse, but it appears that
    ///     React just calls Object.freeze on the children array.
    ///     Object.freeze, historically, is quite a bit slower[0] than just not doing that.
    ///     Given that...I am choosing to always pass "false" to this.
    ///     This also skips extra state that we'd need to track.
    ///     If React Fast Refresh ends up using this later, then we can revisit this decision.
    ///  [0]: https://github.com/automerge/automerge/issues/177
    pub const JSXElement = struct {
        /// JSX tag name
        /// <div> => E.String.init("div")
        /// <MyComponent> => E.Identifier{.ref = symbolPointingToMyComponent }
        /// null represents a fragment
        tag: ?ExprNodeIndex = null,

        /// JSX props
        properties: G.Property.List = G.Property.List{},

        /// JSX element children <div>{this_is_a_child_element}</div>
        children: ExprNodeList = ExprNodeList{},

        // needed to make sure parse and visit happen in the same order
        key_prop_index: i32 = -1,

        flags: Flags.JSXElement.Bitset = Flags.JSXElement.Bitset{},

        close_tag_loc: logger.Loc = logger.Loc.Empty,

        pub const SpecialProp = enum {
            __self, // old react transform used this as a prop
            __source,
            key,
            ref,
            any,

            pub const Map = ComptimeStringMap(SpecialProp, .{
                .{ "__self", .__self },
                .{ "__source", .__source },
                .{ "key", .key },
                .{ "ref", .ref },
            });
        };
    };

    pub const Missing = struct {
        pub fn jsonStringify(_: *const @This(), writer: anytype) !void {
            return try writer.write(null);
        }
    };

    pub const Number = struct {
        value: f64,

        const double_digit = [_]string{ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "100" };
        const neg_double_digit = [_]string{ "-0", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9", "-10", "-11", "-12", "-13", "-14", "-15", "-16", "-17", "-18", "-19", "-20", "-21", "-22", "-23", "-24", "-25", "-26", "-27", "-28", "-29", "-30", "-31", "-32", "-33", "-34", "-35", "-36", "-37", "-38", "-39", "-40", "-41", "-42", "-43", "-44", "-45", "-46", "-47", "-48", "-49", "-50", "-51", "-52", "-53", "-54", "-55", "-56", "-57", "-58", "-59", "-60", "-61", "-62", "-63", "-64", "-65", "-66", "-67", "-68", "-69", "-70", "-71", "-72", "-73", "-74", "-75", "-76", "-77", "-78", "-79", "-80", "-81", "-82", "-83", "-84", "-85", "-86", "-87", "-88", "-89", "-90", "-91", "-92", "-93", "-94", "-95", "-96", "-97", "-98", "-99", "-100" };

        /// String concatenation with numbers is required by the TypeScript compiler for
        /// "constant expression" handling in enums. We can match the behavior of a JS VM
        /// by calling out to the APIs in WebKit which are responsible for this operation.
        ///
        /// This can return `null` in wasm builds to avoid linking JSC
        pub fn toString(this: Number, allocator: std.mem.Allocator) ?string {
            return toStringFromF64(this.value, allocator);
        }

        pub fn toStringFromF64(value: f64, allocator: std.mem.Allocator) ?string {
            if (value == @trunc(value) and (value < std.math.maxInt(i32) and value > std.math.minInt(i32))) {
                const int_value = @as(i64, @intFromFloat(value));
                const abs = @as(u64, @intCast(@abs(int_value)));

                // do not allocate for a small set of constant numbers: -100 through 100
                if (abs < double_digit.len) {
                    return if (int_value < 0)
                        neg_double_digit[abs]
                    else
                        double_digit[abs];
                }

                return std.fmt.allocPrint(allocator, "{d}", .{@as(i32, @intCast(int_value))}) catch return null;
            }

            if (std.math.isNan(value)) {
                return "NaN";
            }

            if (std.math.isNegativeInf(value)) {
                return "-Infinity";
            }

            if (std.math.isInf(value)) {
                return "Infinity";
            }

            if (Environment.isNative) {
                var buf: [124]u8 = undefined;
                return allocator.dupe(u8, bun.fmt.FormatDouble.dtoa(&buf, value)) catch bun.outOfMemory();
            } else {
                // do not attempt to implement the spec here, it would be error prone.
            }

            return null;
        }

        pub inline fn toU64(self: Number) u64 {
            return self.to(u64);
        }

        pub inline fn toUsize(self: Number) usize {
            return self.to(usize);
        }

        pub inline fn toU32(self: Number) u32 {
            return self.to(u32);
        }

        pub inline fn toU16(self: Number) u16 {
            return self.to(u16);
        }

        pub fn to(self: Number, comptime T: type) T {
            return @as(T, @intFromFloat(@min(@max(@trunc(self.value), 0), comptime @min(std.math.floatMax(f64), std.math.maxInt(T)))));
        }

        pub fn jsonStringify(self: *const Number, writer: anytype) !void {
            return try writer.write(self.value);
        }

        pub fn toJS(this: @This()) JSC.JSValue {
            return JSC.JSValue.jsNumber(this.value);
        }
    };

    pub const BigInt = struct {
        value: string,

        pub var empty = BigInt{ .value = "" };

        pub fn jsonStringify(self: *const @This(), writer: anytype) !void {
            return try writer.write(self.value);
        }

        pub fn toJS(_: @This()) JSC.JSValue {
            // TODO:
            return JSC.JSValue.jsNumber(0);
        }
    };

    pub const Object = struct {
        properties: G.Property.List = G.Property.List{},
        comma_after_spread: ?logger.Loc = null,
        is_single_line: bool = false,
        is_parenthesized: bool = false,
        was_originally_macro: bool = false,

        close_brace_loc: logger.Loc = logger.Loc.Empty,

        // used in TOML parser to merge properties
        pub const Rope = struct {
            head: Expr,
            next: ?*Rope = null,
            pub fn append(this: *Rope, expr: Expr, allocator: std.mem.Allocator) OOM!*Rope {
                if (this.next) |next| {
                    return try next.append(expr, allocator);
                }

                const rope = try allocator.create(Rope);
                rope.* = .{ .head = expr };
                this.next = rope;
                return rope;
            }
        };

        pub fn get(self: *const Object, key: string) ?Expr {
            return if (asProperty(self, key)) |query| query.expr else @as(?Expr, null);
        }

        pub fn toJS(this: *Object, allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) ToJSError!JSC.JSValue {
            var obj = JSC.JSValue.createEmptyObject(globalObject, this.properties.len);
            obj.protect();
            defer obj.unprotect();
            const props: []const G.Property = this.properties.slice();
            for (props) |prop| {
                if (prop.kind != .normal or prop.class_static_block != null or prop.key == null or prop.key.?.data != .e_string or prop.value == null) {
                    return error.@"Cannot convert argument type to JS";
                }
                var key = prop.key.?.data.e_string.toZigString(allocator);
                obj.put(globalObject, &key, try prop.value.?.toJS(allocator, globalObject));
            }

            return obj;
        }

        pub fn put(self: *Object, allocator: std.mem.Allocator, key: string, expr: Expr) !void {
            if (asProperty(self, key)) |query| {
                self.properties.ptr[query.i].value = expr;
            } else {
                try self.properties.push(allocator, .{
                    .key = Expr.init(E.String, E.String.init(key), expr.loc),
                    .value = expr,
                });
            }
        }

        pub fn putString(self: *Object, allocator: std.mem.Allocator, key: string, value: string) !void {
            return try put(self, allocator, key, Expr.init(E.String, E.String.init(value), logger.Loc.Empty));
        }

        pub const SetError = error{ OutOfMemory, Clobber };

        pub fn set(self: *const Object, key: Expr, allocator: std.mem.Allocator, value: Expr) SetError!void {
            if (self.hasProperty(key.data.e_string.data)) return error.Clobber;
            try self.properties.push(allocator, .{
                .key = key,
                .value = value,
            });
        }

        pub const RopeQuery = struct {
            expr: Expr,
            rope: *const Rope,
        };

        // this is terribly, shamefully slow
        pub fn setRope(self: *Object, rope: *const Rope, allocator: std.mem.Allocator, value: Expr) SetError!void {
            if (self.get(rope.head.data.e_string.data)) |existing| {
                switch (existing.data) {
                    .e_array => |array| {
                        if (rope.next == null) {
                            try array.push(allocator, value);
                            return;
                        }

                        if (array.items.last()) |last| {
                            if (last.data != .e_object) {
                                return error.Clobber;
                            }

                            try last.data.e_object.setRope(rope.next.?, allocator, value);
                            return;
                        }

                        try array.push(allocator, value);
                        return;
                    },
                    .e_object => |object| {
                        if (rope.next != null) {
                            try object.setRope(rope.next.?, allocator, value);
                            return;
                        }

                        return error.Clobber;
                    },
                    else => {
                        return error.Clobber;
                    },
                }
            }

            var value_ = value;
            if (rope.next) |next| {
                var obj = Expr.init(E.Object, E.Object{ .properties = .{} }, rope.head.loc);
                try obj.data.e_object.setRope(next, allocator, value);
                value_ = obj;
            }

            try self.properties.push(allocator, .{
                .key = rope.head,
                .value = value_,
            });
        }

        pub fn getOrPutObject(self: *Object, rope: *const Rope, allocator: std.mem.Allocator) SetError!Expr {
            if (self.get(rope.head.data.e_string.data)) |existing| {
                switch (existing.data) {
                    .e_array => |array| {
                        if (rope.next == null) {
                            return error.Clobber;
                        }

                        if (array.items.last()) |last| {
                            if (last.data != .e_object) {
                                return error.Clobber;
                            }

                            return try last.data.e_object.getOrPutObject(rope.next.?, allocator);
                        }

                        return error.Clobber;
                    },
                    .e_object => |object| {
                        if (rope.next != null) {
                            return try object.getOrPutObject(rope.next.?, allocator);
                        }

                        // success
                        return existing;
                    },
                    else => {
                        return error.Clobber;
                    },
                }
            }

            if (rope.next) |next| {
                var obj = Expr.init(E.Object, E.Object{ .properties = .{} }, rope.head.loc);
                const out = try obj.data.e_object.getOrPutObject(next, allocator);
                try self.properties.push(allocator, .{
                    .key = rope.head,
                    .value = obj,
                });
                return out;
            }

            const out = Expr.init(E.Object, E.Object{}, rope.head.loc);
            try self.properties.push(allocator, .{
                .key = rope.head,
                .value = out,
            });
            return out;
        }

        pub fn getOrPutArray(self: *Object, rope: *const Rope, allocator: std.mem.Allocator) SetError!Expr {
            if (self.get(rope.head.data.e_string.data)) |existing| {
                switch (existing.data) {
                    .e_array => |array| {
                        if (rope.next == null) {
                            return existing;
                        }

                        if (array.items.last()) |last| {
                            if (last.data != .e_object) {
                                return error.Clobber;
                            }

                            return try last.data.e_object.getOrPutArray(rope.next.?, allocator);
                        }

                        return error.Clobber;
                    },
                    .e_object => |object| {
                        if (rope.next == null) {
                            return error.Clobber;
                        }

                        return try object.getOrPutArray(rope.next.?, allocator);
                    },
                    else => {
                        return error.Clobber;
                    },
                }
            }

            if (rope.next) |next| {
                var obj = Expr.init(E.Object, E.Object{ .properties = .{} }, rope.head.loc);
                const out = try obj.data.e_object.getOrPutArray(next, allocator);
                try self.properties.push(allocator, .{
                    .key = rope.head,
                    .value = obj,
                });
                return out;
            }

            const out = Expr.init(E.Array, E.Array{}, rope.head.loc);
            try self.properties.push(allocator, .{
                .key = rope.head,
                .value = out,
            });
            return out;
        }

        pub fn hasProperty(obj: *const Object, name: string) bool {
            for (obj.properties.slice()) |prop| {
                const key = prop.key orelse continue;
                if (std.meta.activeTag(key.data) != .e_string) continue;
                if (key.data.e_string.eql(string, name)) return true;
            }
            return false;
        }

        pub fn asProperty(obj: *const Object, name: string) ?Expr.Query {
            for (obj.properties.slice(), 0..) |prop, i| {
                const value = prop.value orelse continue;
                const key = prop.key orelse continue;
                if (std.meta.activeTag(key.data) != .e_string) continue;
                const key_str = key.data.e_string;
                if (key_str.eql(string, name)) {
                    return Expr.Query{
                        .expr = value,
                        .loc = key.loc,
                        .i = @as(u32, @truncate(i)),
                    };
                }
            }

            return null;
        }

        /// Assumes each key in the property is a string
        pub fn alphabetizeProperties(this: *Object) void {
            if (comptime Environment.isDebug) {
                for (this.properties.slice()) |prop| {
                    bun.assert(prop.key.?.data == .e_string);
                }
            }
            std.sort.pdq(G.Property, this.properties.slice(), {}, Sorter.isLessThan);
        }

        pub fn packageJSONSort(this: *Object) void {
            std.sort.pdq(G.Property, this.properties.slice(), {}, PackageJSONSort.Fields.isLessThan);
        }

        const PackageJSONSort = struct {
            const Fields = enum(u8) {
                name = 0,
                version = 1,
                author = 2,
                repository = 3,
                config = 4,
                main = 5,
                module = 6,
                dependencies = 7,
                devDependencies = 8,
                optionalDependencies = 9,
                peerDependencies = 10,
                exports = 11,
                __fake = 12,

                pub const Map = ComptimeStringMap(Fields, .{
                    .{ "name", Fields.name },
                    .{ "version", Fields.version },
                    .{ "author", Fields.author },
                    .{ "repository", Fields.repository },
                    .{ "config", Fields.config },
                    .{ "main", Fields.main },
                    .{ "module", Fields.module },
                    .{ "dependencies", Fields.dependencies },
                    .{ "devDependencies", Fields.devDependencies },
                    .{ "optionalDependencies", Fields.optionalDependencies },
                    .{ "peerDependencies", Fields.peerDependencies },
                    .{ "exports", Fields.exports },
                });

                pub fn isLessThan(ctx: void, lhs: G.Property, rhs: G.Property) bool {
                    var lhs_key_size: u8 = @intFromEnum(Fields.__fake);
                    var rhs_key_size: u8 = @intFromEnum(Fields.__fake);

                    if (lhs.key != null and lhs.key.?.data == .e_string) {
                        lhs_key_size = @intFromEnum(Map.get(lhs.key.?.data.e_string.data) orelse Fields.__fake);
                    }

                    if (rhs.key != null and rhs.key.?.data == .e_string) {
                        rhs_key_size = @intFromEnum(Map.get(rhs.key.?.data.e_string.data) orelse Fields.__fake);
                    }

                    return switch (std.math.order(lhs_key_size, rhs_key_size)) {
                        .lt => true,
                        .gt => false,
                        .eq => strings.cmpStringsAsc(ctx, lhs.key.?.data.e_string.data, rhs.key.?.data.e_string.data),
                    };
                }
            };
        };

        const Sorter = struct {
            pub fn isLessThan(ctx: void, lhs: G.Property, rhs: G.Property) bool {
                return strings.cmpStringsAsc(ctx, lhs.key.?.data.e_string.data, rhs.key.?.data.e_string.data);
            }
        };
    };

    pub const Spread = struct { value: ExprNodeIndex };

    /// JavaScript string literal type
    pub const String = struct {
        // A version of this where `utf8` and `value` are stored in a packed union, with len as a single u32 was attempted.
        // It did not improve benchmarks. Neither did converting this from a heap-allocated type to a stack-allocated type.
        // TODO: change this to *const anyopaque and change all uses to either .slice8() or .slice16()
        data: []const u8 = "",
        prefer_template: bool = false,

        // A very simple rope implementation
        // We only use this for string folding, so this is kind of overkill
        // We don't need to deal with substrings
        next: ?*String = null,
        end: ?*String = null,
        rope_len: u32 = 0,
        is_utf16: bool = false,

        pub fn isIdentifier(this: *String, allocator: std.mem.Allocator) bool {
            if (!this.isUTF8()) {
                return bun.js_lexer.isIdentifierUTF16(this.slice16());
            }

            return bun.js_lexer.isIdentifier(this.slice(allocator));
        }

        pub const class = E.String{ .data = "class" };

        pub fn push(this: *String, other: *String) void {
            bun.assert(this.isUTF8());
            bun.assert(other.isUTF8());

            if (other.rope_len == 0) {
                other.rope_len = @truncate(other.data.len);
            }

            if (this.rope_len == 0) {
                this.rope_len = @truncate(this.data.len);
            }

            this.rope_len += other.rope_len;
            if (this.next == null) {
                this.next = other;
                this.end = other;
            } else {
                var end = this.end.?;
                while (end.next != null) end = end.end.?;
                end.next = other;
                this.end = other;
            }
        }

        /// Cloning the rope string is rarely needed, see `foldStringAddition`'s
        /// comments and the 'edgecase/EnumInliningRopeStringPoison' test
        pub fn cloneRopeNodes(s: String) String {
            var root = s;

            if (root.next != null) {
                var current: ?*String = &root;
                while (true) {
                    const node = current.?;
                    if (node.next) |next| {
                        node.next = Expr.Data.Store.append(String, next.*);
                        current = node.next;
                    } else {
                        root.end = node;
                        break;
                    }
                }
            }

            return root;
        }

        pub fn toUTF8(this: *String, allocator: std.mem.Allocator) !void {
            if (!this.is_utf16) return;
            this.data = try strings.toUTF8Alloc(allocator, this.slice16());
            this.is_utf16 = false;
        }

        pub fn init(value: anytype) String {
            const Value = @TypeOf(value);
            if (Value == []u16 or Value == []const u16) {
                return .{
                    .data = @as([*]const u8, @ptrCast(value.ptr))[0..value.len],
                    .is_utf16 = true,
                };
            }

            return .{ .data = value };
        }

        /// E.String containing non-ascii characters may not fully work.
        /// https://github.com/oven-sh/bun/issues/11963
        /// More investigation is needed.
        pub fn initReEncodeUTF8(utf8: []const u8, allocator: std.mem.Allocator) String {
            return if (bun.strings.isAllASCII(utf8))
                init(utf8)
            else
                init(bun.strings.toUTF16AllocForReal(allocator, utf8, false, false) catch bun.outOfMemory());
        }

        pub fn slice8(this: *const String) []const u8 {
            bun.assert(!this.is_utf16);
            return this.data;
        }

        pub fn slice16(this: *const String) []const u16 {
            bun.assert(this.is_utf16);
            return @as([*]const u16, @ptrCast(@alignCast(this.data.ptr)))[0..this.data.len];
        }

        pub fn resolveRopeIfNeeded(this: *String, allocator: std.mem.Allocator) void {
            if (this.next == null or !this.isUTF8()) return;
            var bytes = std.ArrayList(u8).initCapacity(allocator, this.rope_len) catch bun.outOfMemory();

            bytes.appendSliceAssumeCapacity(this.data);
            var str = this.next;
            while (str) |part| {
                bytes.appendSlice(part.data) catch bun.outOfMemory();
                str = part.next;
            }
            this.data = bytes.items;
            this.next = null;
        }

        pub fn slice(this: *String, allocator: std.mem.Allocator) []const u8 {
            this.resolveRopeIfNeeded(allocator);
            return this.string(allocator) catch bun.outOfMemory();
        }

        pub var empty = String{};
        pub var @"true" = String{ .data = "true" };
        pub var @"false" = String{ .data = "false" };
        pub var @"null" = String{ .data = "null" };
        pub var @"undefined" = String{ .data = "undefined" };

        pub fn clone(str: *const String, allocator: std.mem.Allocator) !String {
            return String{
                .data = try allocator.dupe(u8, str.data),
                .prefer_template = str.prefer_template,
                .is_utf16 = !str.isUTF8(),
            };
        }

        pub fn cloneSliceIfNecessary(str: *const String, allocator: std.mem.Allocator) !bun.string {
            if (str.isUTF8()) {
                return allocator.dupe(u8, str.string(allocator) catch unreachable);
            }

            return str.string(allocator);
        }

        pub fn javascriptLength(s: *const String) ?u32 {
            if (s.rope_len > 0) {
                // We only support ascii ropes for now
                return s.rope_len;
            }

            if (s.isUTF8()) {
                if (!strings.isAllASCII(s.data)) {
                    return null;
                }
                return @truncate(s.data.len);
            }

            return @truncate(s.slice16().len);
        }

        pub inline fn len(s: *const String) usize {
            return if (s.rope_len > 0) s.rope_len else s.data.len;
        }

        pub inline fn isUTF8(s: *const String) bool {
            return !s.is_utf16;
        }

        pub inline fn isBlank(s: *const String) bool {
            return s.len() == 0;
        }

        pub inline fn isPresent(s: *const String) bool {
            return s.len() > 0;
        }

        pub fn eql(s: *const String, comptime _t: type, other: anytype) bool {
            if (s.isUTF8()) {
                switch (_t) {
                    @This() => {
                        if (other.isUTF8()) {
                            return strings.eqlLong(s.data, other.data, true);
                        } else {
                            return strings.utf16EqlString(other.slice16(), s.data);
                        }
                    },
                    bun.string => {
                        return strings.eqlLong(s.data, other, true);
                    },
                    []u16, []const u16 => {
                        return strings.utf16EqlString(other, s.data);
                    },
                    else => {
                        @compileError("Invalid type");
                    },
                }
            } else {
                switch (_t) {
                    @This() => {
                        if (other.isUTF8()) {
                            return strings.utf16EqlString(s.slice16(), other.data);
                        } else {
                            return std.mem.eql(u16, other.slice16(), s.slice16());
                        }
                    },
                    bun.string => {
                        return strings.utf16EqlString(s.slice16(), other);
                    },
                    []u16, []const u16 => {
                        return std.mem.eql(u16, other.slice16(), s.slice16());
                    },
                    else => {
                        @compileError("Invalid type");
                    },
                }
            }
        }

        pub fn eqlComptime(s: *const String, comptime value: []const u8) bool {
            bun.assert(s.next == null);
            return if (s.isUTF8())
                strings.eqlComptime(s.data, value)
            else
                strings.eqlComptimeUTF16(s.slice16(), value);
        }

        pub fn hasPrefixComptime(s: *const String, comptime value: anytype) bool {
            if (s.data.len < value.len)
                return false;

            return if (s.isUTF8())
                strings.eqlComptime(s.data[0..value.len], value)
            else
                strings.eqlComptimeUTF16(s.slice16()[0..value.len], value);
        }

        pub fn string(s: *const String, allocator: std.mem.Allocator) OOM!bun.string {
            if (s.isUTF8()) {
                return s.data;
            } else {
                return strings.toUTF8Alloc(allocator, s.slice16());
            }
        }

        pub fn stringZ(s: *const String, allocator: std.mem.Allocator) OOM!bun.stringZ {
            if (s.isUTF8()) {
                return allocator.dupeZ(u8, s.data);
            } else {
                return strings.toUTF8AllocZ(allocator, s.slice16());
            }
        }

        pub fn stringCloned(s: *const String, allocator: std.mem.Allocator) OOM!bun.string {
            if (s.isUTF8()) {
                return allocator.dupe(u8, s.data);
            } else {
                return strings.toUTF8Alloc(allocator, s.slice16());
            }
        }

        pub fn hash(s: *const String) u64 {
            if (s.isBlank()) return 0;

            if (s.isUTF8()) {
                // hash utf-8
                return bun.hash(s.data);
            } else {
                // hash utf-16
                return bun.hash(@as([*]const u8, @ptrCast(s.slice16().ptr))[0 .. s.slice16().len * 2]);
            }
        }

        pub fn toJS(s: *String, allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) !JSC.JSValue {
            s.resolveRopeIfNeeded(allocator);
            if (!s.isPresent()) {
                var emp = bun.String.empty;
                return emp.toJS(globalObject);
            }

            if (s.isUTF8()) {
                if (try strings.toUTF16Alloc(allocator, s.slice8(), false, false)) |utf16| {
                    var out, const chars = bun.String.createUninitialized(.utf16, utf16.len);
                    @memcpy(chars, utf16);
                    return out.transferToJS(globalObject);
                } else {
                    var out, const chars = bun.String.createUninitialized(.latin1, s.slice8().len);
                    @memcpy(chars, s.slice8());
                    return out.transferToJS(globalObject);
                }
            } else {
                var out, const chars = bun.String.createUninitialized(.utf16, s.slice16().len);
                @memcpy(chars, s.slice16());
                return out.transferToJS(globalObject);
            }
        }

        pub fn toZigString(s: *String, allocator: std.mem.Allocator) JSC.ZigString {
            if (s.isUTF8()) {
                return JSC.ZigString.fromUTF8(s.slice(allocator));
            } else {
                return JSC.ZigString.initUTF16(s.slice16());
            }
        }

        pub fn format(s: String, comptime fmt: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
            comptime bun.assert(fmt.len == 0);

            try writer.writeAll("E.String");
            if (s.next == null) {
                try writer.writeAll("(");
                if (s.isUTF8()) {
                    try writer.print("\"{s}\"", .{s.data});
                } else {
                    try writer.print("\"{}\"", .{bun.fmt.utf16(s.slice16())});
                }
                try writer.writeAll(")");
            } else {
                try writer.writeAll("(rope: [");
                var it: ?*const String = &s;
                while (it) |part| {
                    if (part.isUTF8()) {
                        try writer.print("\"{s}\"", .{part.data});
                    } else {
                        try writer.print("\"{}\"", .{bun.fmt.utf16(part.slice16())});
                    }
                    it = part.next;
                    if (it != null) try writer.writeAll(" ");
                }
                try writer.writeAll("])");
            }
        }

        pub fn jsonStringify(s: *const String, writer: anytype) !void {
            var buf = [_]u8{0} ** 4096;
            var i: usize = 0;
            for (s.slice16()) |char| {
                buf[i] = @as(u8, @intCast(char));
                i += 1;
                if (i >= 4096) {
                    break;
                }
            }

            return try writer.write(buf[0..i]);
        }
    };

    // value is in the Node
    pub const TemplatePart = struct {
        value: ExprNodeIndex,
        tail_loc: logger.Loc,
        tail: Template.Contents,
    };

    pub const Template = struct {
        tag: ?ExprNodeIndex = null,
        parts: []TemplatePart = &.{},
        head: Contents,

        pub const Contents = union(Tag) {
            cooked: E.String,
            raw: string,

            const Tag = enum {
                cooked,
                raw,
            };

            pub fn isUTF8(contents: Contents) bool {
                return contents == .cooked and contents.cooked.isUTF8();
            }
        };

        /// "`a${'b'}c`" => "`abc`"
        pub fn fold(
            this: *Template,
            allocator: std.mem.Allocator,
            loc: logger.Loc,
        ) Expr {
            if (this.tag != null or (this.head == .cooked and !this.head.cooked.isUTF8())) {
                // we only fold utf-8/ascii for now
                return Expr{
                    .data = .{ .e_template = this },
                    .loc = loc,
                };
            }

            bun.assert(this.head == .cooked);

            if (this.parts.len == 0) {
                return Expr.init(E.String, this.head.cooked, loc);
            }

            var parts = std.ArrayList(TemplatePart).initCapacity(allocator, this.parts.len) catch unreachable;
            var head = Expr.init(E.String, this.head.cooked, loc);
            for (this.parts) |part_src| {
                var part = part_src;
                bun.assert(part.tail == .cooked);

                part.value = part.value.unwrapInlined();

                switch (part.value.data) {
                    .e_number => {
                        if (part.value.data.e_number.toString(allocator)) |s| {
                            part.value = Expr.init(E.String, E.String.init(s), part.value.loc);
                        }
                    },
                    .e_null => {
                        part.value = Expr.init(E.String, E.String.init("null"), part.value.loc);
                    },
                    .e_boolean => {
                        part.value = Expr.init(E.String, E.String.init(if (part.value.data.e_boolean.value)
                            "true"
                        else
                            "false"), part.value.loc);
                    },
                    .e_undefined => {
                        part.value = Expr.init(E.String, E.String.init("undefined"), part.value.loc);
                    },
                    .e_big_int => |value| {
                        part.value = Expr.init(E.String, E.String.init(value.value), part.value.loc);
                    },
                    else => {},
                }

                if (part.value.data == .e_string and part.tail.cooked.isUTF8() and part.value.data.e_string.isUTF8()) {
                    if (parts.items.len == 0) {
                        if (part.value.data.e_string.len() > 0) {
                            head.data.e_string.push(Expr.init(E.String, part.value.data.e_string.*, logger.Loc.Empty).data.e_string);
                        }

                        if (part.tail.cooked.len() > 0) {
                            head.data.e_string.push(Expr.init(E.String, part.tail.cooked, part.tail_loc).data.e_string);
                        }

                        continue;
                    } else {
                        var prev_part = &parts.items[parts.items.len - 1];
                        bun.assert(prev_part.tail == .cooked);

                        if (prev_part.tail.cooked.isUTF8()) {
                            if (part.value.data.e_string.len() > 0) {
                                prev_part.tail.cooked.push(Expr.init(E.String, part.value.data.e_string.*, logger.Loc.Empty).data.e_string);
                            }

                            if (part.tail.cooked.len() > 0) {
                                prev_part.tail.cooked.push(Expr.init(E.String, part.tail.cooked, part.tail_loc).data.e_string);
                            }
                        } else {
                            parts.appendAssumeCapacity(part);
                        }
                    }
                } else {
                    parts.appendAssumeCapacity(part);
                }
            }

            if (parts.items.len == 0) {
                parts.deinit();
                head.data.e_string.resolveRopeIfNeeded(allocator);
                return head;
            }

            return Expr.init(E.Template, .{
                .tag = null,
                .parts = parts.items,
                .head = .{ .cooked = head.data.e_string.* },
            }, loc);
        }
    };

    pub const RegExp = struct {
        value: string,

        // This exists for JavaScript bindings
        // The RegExp constructor expects flags as a second argument.
        // We want to avoid re-lexing the flags, so we store them here.
        // This is the index of the first character in a flag, not the "/"
        // /foo/gim
        //      ^
        flags_offset: ?u16 = null,

        pub var empty = RegExp{ .value = "" };

        pub fn pattern(this: RegExp) string {

            // rewind until we reach the /foo/gim
            //                               ^
            // should only ever be a single character
            // but we're being cautious
            if (this.flags_offset) |i_| {
                var i = i_;
                while (i > 0 and this.value[i] != '/') {
                    i -= 1;
                }

                return std.mem.trim(u8, this.value[0..i], "/");
            }

            return std.mem.trim(u8, this.value, "/");
        }

        pub fn flags(this: RegExp) string {
            // rewind until we reach the /foo/gim
            //                               ^
            // should only ever be a single character
            // but we're being cautious
            if (this.flags_offset) |i| {
                return this.value[i..];
            }

            return "";
        }

        pub fn jsonStringify(self: *const RegExp, writer: anytype) !void {
            return try writer.write(self.value);
        }
    };

    pub const Class = G.Class;

    pub const Await = struct {
        value: ExprNodeIndex,
    };

    pub const Yield = struct {
        value: ?ExprNodeIndex = null,
        is_star: bool = false,
    };

    pub const If = struct {
        test_: ExprNodeIndex,
        yes: ExprNodeIndex,
        no: ExprNodeIndex,
    };

    pub const RequireString = struct {
        import_record_index: u32 = 0,

        unwrapped_id: u32 = std.math.maxInt(u32),
    };

    pub const RequireResolveString = struct {
        import_record_index: u32,

        // close_paren_loc: logger.Loc = logger.Loc.Empty,
    };

    pub const InlinedEnum = struct {
        value: ExprNodeIndex,
        comment: string,
    };

    pub const Import = struct {
        expr: ExprNodeIndex,
        options: ExprNodeIndex = Expr.empty,
        import_record_index: u32,

        /// TODO:
        /// Comments inside "import()" expressions have special meaning for Webpack.
        /// Preserving comments inside these expressions makes it possible to use
        /// esbuild as a TypeScript-to-JavaScript frontend for Webpack to improve
        /// performance. We intentionally do not interpret these comments in esbuild
        /// because esbuild is not Webpack. But we do preserve them since doing so is
        /// harmless, easy to maintain, and useful to people. See the Webpack docs for
        /// more info: https://webpack.js.org/api/module-methods/#magic-comments.
        // leading_interior_comments: []G.Comment = &([_]G.Comment{}),

        pub fn isImportRecordNull(this: *const Import) bool {
            return this.import_record_index == std.math.maxInt(u32);
        }

        pub fn importRecordLoader(import: *const Import) ?bun.options.Loader {
            // This logic is duplicated in js_printer.zig fn parsePath()
            const obj = import.options.data.as(.e_object) orelse
                return null;
            const with = obj.get("with") orelse obj.get("assert") orelse
                return null;
            const with_obj = with.data.as(.e_object) orelse
                return null;
            const str = (with_obj.get("type") orelse
                return null).data.as(.e_string) orelse
                return null;

            if (!str.is_utf16) if (bun.options.Loader.fromString(str.data)) |loader| {
                if (loader == .sqlite) {
                    const embed = with_obj.get("embed") orelse return loader;
                    const embed_str = embed.data.as(.e_string) orelse return loader;
                    if (embed_str.eqlComptime("true")) {
                        return .sqlite_embedded;
                    }
                }
                return loader;
            };

            return null;
        }
    };
};

pub const Stmt = struct {
    loc: logger.Loc,
    data: Data,

    pub const Batcher = NewBatcher(Stmt);

    pub fn assign(a: Expr, b: Expr) Stmt {
        return Stmt.alloc(
            S.SExpr,
            S.SExpr{
                .value = Expr.assign(a, b),
            },
            a.loc,
        );
    }

    const Serializable = struct {
        type: Tag,
        object: string,
        value: Data,
        loc: logger.Loc,
    };

    pub fn jsonStringify(self: *const Stmt, writer: anytype) !void {
        return try writer.write(Serializable{ .type = std.meta.activeTag(self.data), .object = "stmt", .value = self.data, .loc = self.loc });
    }

    pub fn isTypeScript(self: *Stmt) bool {
        return @as(Stmt.Tag, self.data) == .s_type_script;
    }

    pub fn isSuperCall(self: Stmt) bool {
        return self.data == .s_expr and self.data.s_expr.value.data == .e_call and self.data.s_expr.value.data.e_call.target.data == .e_super;
    }

    pub fn isMissingExpr(self: Stmt) bool {
        return self.data == .s_expr and self.data.s_expr.value.data == .e_missing;
    }

    pub fn empty() Stmt {
        return Stmt{ .data = .{ .s_empty = None }, .loc = logger.Loc{} };
    }

    pub fn toEmpty(this: Stmt) Stmt {
        return .{
            .data = .{
                .s_empty = None,
            },
            .loc = this.loc,
        };
    }

    const None = S.Empty{};

    pub var icount: usize = 0;
    pub fn init(comptime StatementType: type, origData: *StatementType, loc: logger.Loc) Stmt {
        icount += 1;

        return switch (comptime StatementType) {
            S.Empty => Stmt{ .loc = loc, .data = Data{ .s_empty = S.Empty{} } },
            S.Block => Stmt.comptime_init("s_block", S.Block, origData, loc),
            S.Break => Stmt.comptime_init("s_break", S.Break, origData, loc),
            S.Class => Stmt.comptime_init("s_class", S.Class, origData, loc),
            S.Comment => Stmt.comptime_init("s_comment", S.Comment, origData, loc),
            S.Continue => Stmt.comptime_init("s_continue", S.Continue, origData, loc),
            S.Debugger => Stmt.comptime_init("s_debugger", S.Debugger, origData, loc),
            S.Directive => Stmt.comptime_init("s_directive", S.Directive, origData, loc),
            S.DoWhile => Stmt.comptime_init("s_do_while", S.DoWhile, origData, loc),
            S.Enum => Stmt.comptime_init("s_enum", S.Enum, origData, loc),
            S.ExportClause => Stmt.comptime_init("s_export_clause", S.ExportClause, origData, loc),
            S.ExportDefault => Stmt.comptime_init("s_export_default", S.ExportDefault, origData, loc),
            S.ExportEquals => Stmt.comptime_init("s_export_equals", S.ExportEquals, origData, loc),
            S.ExportFrom => Stmt.comptime_init("s_export_from", S.ExportFrom, origData, loc),
            S.ExportStar => Stmt.comptime_init("s_export_star", S.ExportStar, origData, loc),
            S.SExpr => Stmt.comptime_init("s_expr", S.SExpr, origData, loc),
            S.ForIn => Stmt.comptime_init("s_for_in", S.ForIn, origData, loc),
            S.ForOf => Stmt.comptime_init("s_for_of", S.ForOf, origData, loc),
            S.For => Stmt.comptime_init("s_for", S.For, origData, loc),
            S.Function => Stmt.comptime_init("s_function", S.Function, origData, loc),
            S.If => Stmt.comptime_init("s_if", S.If, origData, loc),
            S.Import => Stmt.comptime_init("s_import", S.Import, origData, loc),
            S.Label => Stmt.comptime_init("s_label", S.Label, origData, loc),
            S.Local => Stmt.comptime_init("s_local", S.Local, origData, loc),
            S.Namespace => Stmt.comptime_init("s_namespace", S.Namespace, origData, loc),
            S.Return => Stmt.comptime_init("s_return", S.Return, origData, loc),
            S.Switch => Stmt.comptime_init("s_switch", S.Switch, origData, loc),
            S.Throw => Stmt.comptime_init("s_throw", S.Throw, origData, loc),
            S.Try => Stmt.comptime_init("s_try", S.Try, origData, loc),
            S.TypeScript => Stmt.comptime_init("s_type_script", S.TypeScript, origData, loc),
            S.While => Stmt.comptime_init("s_while", S.While, origData, loc),
            S.With => Stmt.comptime_init("s_with", S.With, origData, loc),
            else => @compileError("Invalid type in Stmt.init"),
        };
    }
    inline fn comptime_alloc(comptime tag_name: string, comptime typename: type, origData: anytype, loc: logger.Loc) Stmt {
        return Stmt{
            .loc = loc,
            .data = @unionInit(
                Data,
                tag_name,
                Data.Store.append(
                    typename,
                    origData,
                ),
            ),
        };
    }

    fn allocateData(allocator: std.mem.Allocator, comptime tag_name: string, comptime typename: type, origData: anytype, loc: logger.Loc) Stmt {
        const value = allocator.create(@TypeOf(origData)) catch unreachable;
        value.* = origData;

        return comptime_init(tag_name, *typename, value, loc);
    }

    inline fn comptime_init(comptime tag_name: string, comptime TypeName: type, origData: TypeName, loc: logger.Loc) Stmt {
        return Stmt{ .loc = loc, .data = @unionInit(Data, tag_name, origData) };
    }

    pub fn alloc(comptime StatementData: type, origData: StatementData, loc: logger.Loc) Stmt {
        Stmt.Data.Store.assert();

        icount += 1;
        return switch (StatementData) {
            S.Block => Stmt.comptime_alloc("s_block", S.Block, origData, loc),
            S.Break => Stmt.comptime_alloc("s_break", S.Break, origData, loc),
            S.Class => Stmt.comptime_alloc("s_class", S.Class, origData, loc),
            S.Comment => Stmt.comptime_alloc("s_comment", S.Comment, origData, loc),
            S.Continue => Stmt.comptime_alloc("s_continue", S.Continue, origData, loc),
            S.Debugger => Stmt{ .loc = loc, .data = .{ .s_debugger = origData } },
            S.Directive => Stmt.comptime_alloc("s_directive", S.Directive, origData, loc),
            S.DoWhile => Stmt.comptime_alloc("s_do_while", S.DoWhile, origData, loc),
            S.Empty => Stmt{ .loc = loc, .data = Data{ .s_empty = S.Empty{} } },
            S.Enum => Stmt.comptime_alloc("s_enum", S.Enum, origData, loc),
            S.ExportClause => Stmt.comptime_alloc("s_export_clause", S.ExportClause, origData, loc),
            S.ExportDefault => Stmt.comptime_alloc("s_export_default", S.ExportDefault, origData, loc),
            S.ExportEquals => Stmt.comptime_alloc("s_export_equals", S.ExportEquals, origData, loc),
            S.ExportFrom => Stmt.comptime_alloc("s_export_from", S.ExportFrom, origData, loc),
            S.ExportStar => Stmt.comptime_alloc("s_export_star", S.ExportStar, origData, loc),
            S.SExpr => Stmt.comptime_alloc("s_expr", S.SExpr, origData, loc),
            S.ForIn => Stmt.comptime_alloc("s_for_in", S.ForIn, origData, loc),
            S.ForOf => Stmt.comptime_alloc("s_for_of", S.ForOf, origData, loc),
            S.For => Stmt.comptime_alloc("s_for", S.For, origData, loc),
            S.Function => Stmt.comptime_alloc("s_function", S.Function, origData, loc),
            S.If => Stmt.comptime_alloc("s_if", S.If, origData, loc),
            S.Import => Stmt.comptime_alloc("s_import", S.Import, origData, loc),
            S.Label => Stmt.comptime_alloc("s_label", S.Label, origData, loc),
            S.Local => Stmt.comptime_alloc("s_local", S.Local, origData, loc),
            S.Namespace => Stmt.comptime_alloc("s_namespace", S.Namespace, origData, loc),
            S.Return => Stmt.comptime_alloc("s_return", S.Return, origData, loc),
            S.Switch => Stmt.comptime_alloc("s_switch", S.Switch, origData, loc),
            S.Throw => Stmt.comptime_alloc("s_throw", S.Throw, origData, loc),
            S.Try => Stmt.comptime_alloc("s_try", S.Try, origData, loc),
            S.TypeScript => Stmt{ .loc = loc, .data = Data{ .s_type_script = S.TypeScript{} } },
            S.While => Stmt.comptime_alloc("s_while", S.While, origData, loc),
            S.With => Stmt.comptime_alloc("s_with", S.With, origData, loc),
            else => @compileError("Invalid type in Stmt.init"),
        };
    }

    pub const Disabler = bun.DebugOnlyDisabler(@This());

    /// When the lifetime of an Stmt.Data's pointer must exist longer than reset() is called, use this function.
    /// Be careful to free the memory (or use an allocator that does it for you)
    /// Also, prefer Stmt.init or Stmt.alloc when possible. This will be slower.
    pub fn allocate(allocator: std.mem.Allocator, comptime StatementData: type, origData: StatementData, loc: logger.Loc) Stmt {
        Stmt.Data.Store.assert();

        icount += 1;
        return switch (StatementData) {
            S.Block => Stmt.allocateData(allocator, "s_block", S.Block, origData, loc),
            S.Break => Stmt.allocateData(allocator, "s_break", S.Break, origData, loc),
            S.Class => Stmt.allocateData(allocator, "s_class", S.Class, origData, loc),
            S.Comment => Stmt.allocateData(allocator, "s_comment", S.Comment, origData, loc),
            S.Continue => Stmt.allocateData(allocator, "s_continue", S.Continue, origData, loc),
            S.Debugger => Stmt{ .loc = loc, .data = .{ .s_debugger = origData } },
            S.Directive => Stmt.allocateData(allocator, "s_directive", S.Directive, origData, loc),
            S.DoWhile => Stmt.allocateData(allocator, "s_do_while", S.DoWhile, origData, loc),
            S.Empty => Stmt{ .loc = loc, .data = Data{ .s_empty = S.Empty{} } },
            S.Enum => Stmt.allocateData(allocator, "s_enum", S.Enum, origData, loc),
            S.ExportClause => Stmt.allocateData(allocator, "s_export_clause", S.ExportClause, origData, loc),
            S.ExportDefault => Stmt.allocateData(allocator, "s_export_default", S.ExportDefault, origData, loc),
            S.ExportEquals => Stmt.allocateData(allocator, "s_export_equals", S.ExportEquals, origData, loc),
            S.ExportFrom => Stmt.allocateData(allocator, "s_export_from", S.ExportFrom, origData, loc),
            S.ExportStar => Stmt.allocateData(allocator, "s_export_star", S.ExportStar, origData, loc),
            S.SExpr => Stmt.allocateData(allocator, "s_expr", S.SExpr, origData, loc),
            S.ForIn => Stmt.allocateData(allocator, "s_for_in", S.ForIn, origData, loc),
            S.ForOf => Stmt.allocateData(allocator, "s_for_of", S.ForOf, origData, loc),
            S.For => Stmt.allocateData(allocator, "s_for", S.For, origData, loc),
            S.Function => Stmt.allocateData(allocator, "s_function", S.Function, origData, loc),
            S.If => Stmt.allocateData(allocator, "s_if", S.If, origData, loc),
            S.Import => Stmt.allocateData(allocator, "s_import", S.Import, origData, loc),
            S.Label => Stmt.allocateData(allocator, "s_label", S.Label, origData, loc),
            S.Local => Stmt.allocateData(allocator, "s_local", S.Local, origData, loc),
            S.Namespace => Stmt.allocateData(allocator, "s_namespace", S.Namespace, origData, loc),
            S.Return => Stmt.allocateData(allocator, "s_return", S.Return, origData, loc),
            S.Switch => Stmt.allocateData(allocator, "s_switch", S.Switch, origData, loc),
            S.Throw => Stmt.allocateData(allocator, "s_throw", S.Throw, origData, loc),
            S.Try => Stmt.allocateData(allocator, "s_try", S.Try, origData, loc),
            S.TypeScript => Stmt{ .loc = loc, .data = Data{ .s_type_script = S.TypeScript{} } },
            S.While => Stmt.allocateData(allocator, "s_while", S.While, origData, loc),
            S.With => Stmt.allocateData(allocator, "s_with", S.With, origData, loc),
            else => @compileError("Invalid type in Stmt.init"),
        };
    }

    pub fn allocateExpr(allocator: std.mem.Allocator, expr: Expr) Stmt {
        return Stmt.allocate(allocator, S.SExpr, S.SExpr{ .value = expr }, expr.loc);
    }

    pub const Tag = enum {
        s_block,
        s_break,
        s_class,
        s_comment,
        s_continue,
        s_directive,
        s_do_while,
        s_enum,
        s_export_clause,
        s_export_default,
        s_export_equals,
        s_export_from,
        s_export_star,
        s_expr,
        s_for_in,
        s_for_of,
        s_for,
        s_function,
        s_if,
        s_import,
        s_label,
        s_local,
        s_namespace,
        s_return,
        s_switch,
        s_throw,
        s_try,
        s_while,
        s_with,
        s_type_script,
        s_empty,
        s_debugger,
        s_lazy_export,

        pub fn jsonStringify(self: @This(), writer: anytype) !void {
            return try writer.write(@tagName(self));
        }

        pub fn isExportLike(tag: Tag) bool {
            return switch (tag) {
                .s_export_clause, .s_export_default, .s_export_equals, .s_export_from, .s_export_star, .s_empty => true,
                else => false,
            };
        }
    };

    pub const Data = union(Tag) {
        s_block: *S.Block,
        s_break: *S.Break,
        s_class: *S.Class,
        s_comment: *S.Comment,
        s_continue: *S.Continue,
        s_directive: *S.Directive,
        s_do_while: *S.DoWhile,
        s_enum: *S.Enum,
        s_export_clause: *S.ExportClause,
        s_export_default: *S.ExportDefault,
        s_export_equals: *S.ExportEquals,
        s_export_from: *S.ExportFrom,
        s_export_star: *S.ExportStar,
        s_expr: *S.SExpr,
        s_for_in: *S.ForIn,
        s_for_of: *S.ForOf,
        s_for: *S.For,
        s_function: *S.Function,
        s_if: *S.If,
        s_import: *S.Import,
        s_label: *S.Label,
        s_local: *S.Local,
        s_namespace: *S.Namespace,
        s_return: *S.Return,
        s_switch: *S.Switch,
        s_throw: *S.Throw,
        s_try: *S.Try,
        s_while: *S.While,
        s_with: *S.With,

        s_type_script: S.TypeScript,
        s_empty: S.Empty, // special case, its a zero value type
        s_debugger: S.Debugger,

        s_lazy_export: *Expr.Data,

        comptime {
            if (@sizeOf(Stmt) > 24) {
                @compileLog("Expected Stmt to be <= 24 bytes, but it is", @sizeOf(Stmt), " bytes");
            }
        }

        pub const Store = struct {
            const StoreType = NewStore(&.{
                S.Block,
                S.Break,
                S.Class,
                S.Comment,
                S.Continue,
                S.Directive,
                S.DoWhile,
                S.Enum,
                S.ExportClause,
                S.ExportDefault,
                S.ExportEquals,
                S.ExportFrom,
                S.ExportStar,
                S.SExpr,
                S.ForIn,
                S.ForOf,
                S.For,
                S.Function,
                S.If,
                S.Import,
                S.Label,
                S.Local,
                S.Namespace,
                S.Return,
                S.Switch,
                S.Throw,
                S.Try,
                S.While,
                S.With,
            }, 128);

            pub threadlocal var instance: ?*StoreType = null;
            pub threadlocal var memory_allocator: ?*ASTMemoryAllocator = null;
            pub threadlocal var disable_reset = false;

            pub fn create() void {
                if (instance != null or memory_allocator != null) {
                    return;
                }

                instance = StoreType.init();
            }

            /// create || reset
            pub fn begin() void {
                if (memory_allocator != null) return;
                if (instance == null) {
                    create();
                    return;
                }

                if (!disable_reset)
                    instance.?.reset();
            }

            pub fn reset() void {
                if (disable_reset or memory_allocator != null) return;
                instance.?.reset();
            }

            pub fn deinit() void {
                if (instance == null or memory_allocator != null) return;
                instance.?.deinit();
                instance = null;
            }

            pub inline fn assert() void {
                if (comptime Environment.allow_assert) {
                    if (instance == null and memory_allocator == null)
                        bun.unreachablePanic("Store must be init'd", .{});
                }
            }

            pub fn append(comptime T: type, value: T) *T {
                if (memory_allocator) |allocator| {
                    return allocator.append(T, value);
                }

                Disabler.assert();
                return instance.?.append(T, value);
            }
        };
    };

    pub fn StoredData(tag: Tag) type {
        const T = @FieldType(Data, tag);
        return switch (@typeInfo(T)) {
            .pointer => |ptr| ptr.child,
            else => T,
        };
    }

    pub fn caresAboutScope(self: *Stmt) bool {
        return switch (self.data) {
            .s_block, .s_empty, .s_debugger, .s_expr, .s_if, .s_for, .s_for_in, .s_for_of, .s_do_while, .s_while, .s_with, .s_try, .s_switch, .s_return, .s_throw, .s_break, .s_continue, .s_directive => {
                return false;
            },

            .s_local => |local| {
                return local.kind != .k_var;
            },
            else => {
                return true;
            },
        };
    }
};

pub const Expr = struct {
    loc: logger.Loc,
    data: Data,

    pub const empty = Expr{ .data = .{ .e_missing = E.Missing{} }, .loc = logger.Loc.Empty };

    pub fn isAnonymousNamed(expr: Expr) bool {
        return switch (expr.data) {
            .e_arrow => true,
            .e_function => |func| func.func.name == null,
            .e_class => |class| class.class_name == null,
            else => false,
        };
    }

    pub fn clone(this: Expr, allocator: std.mem.Allocator) !Expr {
        return .{
            .loc = this.loc,
            .data = try this.data.clone(allocator),
        };
    }

    pub fn deepClone(this: Expr, allocator: std.mem.Allocator) anyerror!Expr {
        return .{
            .loc = this.loc,
            .data = try this.data.deepClone(allocator),
        };
    }

    pub fn wrapInArrow(this: Expr, allocator: std.mem.Allocator) !Expr {
        var stmts = try allocator.alloc(Stmt, 1);
        stmts[0] = Stmt.alloc(S.Return, S.Return{ .value = this }, this.loc);

        return Expr.init(E.Arrow, E.Arrow{
            .args = &.{},
            .body = .{
                .loc = this.loc,
                .stmts = stmts,
            },
        }, this.loc);
    }

    pub fn canBeInlinedFromPropertyAccess(this: Expr) bool {
        return switch (this.data) {
            // if the array has a spread we must keep it
            // https://github.com/oven-sh/bun/issues/2594
            .e_spread => false,

            .e_missing => false,
            else => true,
        };
    }

    pub fn canBeConstValue(this: Expr) bool {
        return this.data.canBeConstValue();
    }

    pub fn canBeMoved(expr: Expr) bool {
        return expr.data.canBeMoved();
    }

    pub fn unwrapInlined(expr: Expr) Expr {
        if (expr.data.as(.e_inlined_enum)) |inlined| return inlined.value;
        return expr;
    }

    pub fn fromBlob(
        blob: *const JSC.WebCore.Blob,
        allocator: std.mem.Allocator,
        mime_type_: ?MimeType,
        log: *logger.Log,
        loc: logger.Loc,
    ) !Expr {
        const bytes = blob.sharedView();

        const mime_type = mime_type_ orelse MimeType.init(blob.content_type, null, null);

        if (mime_type.category == .json) {
            var source = logger.Source.initPathString("fetch.json", bytes);
            var out_expr = JSONParser.parseForMacro(&source, log, allocator) catch {
                return error.MacroFailed;
            };
            out_expr.loc = loc;

            switch (out_expr.data) {
                .e_object => {
                    out_expr.data.e_object.was_originally_macro = true;
                },
                .e_array => {
                    out_expr.data.e_array.was_originally_macro = true;
                },
                else => {},
            }

            return out_expr;
        }

        if (mime_type.category.isTextLike()) {
            var output = MutableString.initEmpty(allocator);
            output = try JSPrinter.quoteForJSON(bytes, output, true);
            var list = output.toOwnedSlice();
            // remove the quotes
            if (list.len > 0) {
                list = list[1 .. list.len - 1];
            }
            return Expr.init(E.String, E.String.init(list), loc);
        }

        return Expr.init(
            E.String,
            E.String{
                .data = try JSC.ZigString.init(bytes).toBase64DataURL(allocator),
            },
            loc,
        );
    }

    pub inline fn initIdentifier(ref: Ref, loc: logger.Loc) Expr {
        return Expr{
            .loc = loc,
            .data = .{
                .e_identifier = E.Identifier.init(ref),
            },
        };
    }

    pub fn toEmpty(expr: Expr) Expr {
        return Expr{ .data = .{ .e_missing = E.Missing{} }, .loc = expr.loc };
    }
    pub fn isEmpty(expr: Expr) bool {
        return expr.data == .e_missing;
    }
    pub const Query = struct { expr: Expr, loc: logger.Loc, i: u32 = 0 };

    pub fn hasAnyPropertyNamed(expr: *const Expr, comptime names: []const string) bool {
        if (std.meta.activeTag(expr.data) != .e_object) return false;
        const obj = expr.data.e_object;
        if (obj.properties.len == 0) return false;

        for (obj.properties.slice()) |prop| {
            if (prop.value == null) continue;
            const key = prop.key orelse continue;
            if (std.meta.activeTag(key.data) != .e_string) continue;
            const key_str = key.data.e_string;
            if (strings.eqlAnyComptime(key_str.data, names)) return true;
        }

        return false;
    }

    pub fn toJS(this: Expr, allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) ToJSError!JSC.JSValue {
        return this.data.toJS(allocator, globalObject);
    }

    pub inline fn isArray(this: *const Expr) bool {
        return this.data == .e_array;
    }

    pub inline fn isObject(this: *const Expr) bool {
        return this.data == .e_object;
    }

    pub fn get(expr: *const Expr, name: string) ?Expr {
        return if (asProperty(expr, name)) |query| query.expr else null;
    }

    /// Only use this for pretty-printing JSON. Do not use in transpiler.
    ///
    /// This does not handle edgecases like `-1` or stringifying arbitrary property lookups.
    pub fn getByIndex(expr: *const Expr, index: u32, index_str: string, allocator: std.mem.Allocator) ?Expr {
        switch (expr.data) {
            .e_array => |array| {
                if (index >= array.items.len) return null;
                return array.items.slice()[index];
            },
            .e_object => |object| {
                for (object.properties.sliceConst()) |*prop| {
                    const key = &(prop.key orelse continue);
                    switch (key.data) {
                        .e_string => |str| {
                            if (str.eql(string, index_str)) {
                                return prop.value;
                            }
                        },
                        .e_number => |num| {
                            if (num.toU32() == index) {
                                return prop.value;
                            }
                        },
                        else => {},
                    }
                }

                return null;
            },
            .e_string => |str| {
                if (str.len() > index) {
                    var slice = str.slice(allocator);
                    // TODO: this is not correct since .length refers to UTF-16 code units and not UTF-8 bytes
                    // However, since this is only used in the JSON prettifier for `bun pm view`, it's not a blocker for shipping.
                    if (slice.len > index) {
                        return Expr.init(E.String, .{ .data = slice[index..][0..1] }, expr.loc);
                    }
                }
            },
            else => {},
        }

        return null;
    }

    /// This supports lookups like:
    /// - `foo`
    /// - `foo.bar`
    /// - `foo[123]`
    /// - `foo[123].bar`
    /// - `foo[123].bar[456]`
    /// - `foo[123].bar[456].baz`
    /// - `foo[123].bar[456].baz.qux` // etc.
    ///
    /// This is not intended for use by the transpiler, instead by pretty printing JSON.
    pub fn getPathMayBeIndex(expr: *const Expr, name: string) ?Expr {
        if (name.len == 0) {
            return null;
        }

        if (strings.indexOfAny(name, "[.")) |idx| {
            switch (name[idx]) {
                '[' => {
                    const end_idx = strings.indexOfChar(name, ']') orelse return null;
                    var base_expr = expr;
                    if (idx > 0) {
                        const key = name[0..idx];
                        base_expr = &(base_expr.get(key) orelse return null);
                    }

                    const index_str = name[idx + 1 .. end_idx];
                    const index = std.fmt.parseInt(u32, index_str, 10) catch return null;
                    const rest = if (name.len > end_idx) name[end_idx + 1 ..] else "";
                    const result = &(base_expr.getByIndex(index, index_str, bun.default_allocator) orelse return null);
                    if (rest.len > 0) return result.getPathMayBeIndex(rest);
                    return result.*;
                },
                '.' => {
                    const key = name[0..idx];
                    const sub_expr = &(expr.get(key) orelse return null);
                    const subpath = if (name.len > idx) name[idx + 1 ..] else "";
                    if (subpath.len > 0) {
                        return sub_expr.getPathMayBeIndex(subpath);
                    }

                    return sub_expr.*;
                },
                else => unreachable,
            }
        }

        return expr.get(name);
    }

    /// Don't use this if you care about performance.
    ///
    /// Sets the value of a property, creating it if it doesn't exist.
    /// `expr` must be an object.
    pub fn set(expr: *Expr, allocator: std.mem.Allocator, name: string, value: Expr) OOM!void {
        bun.assertWithLocation(expr.isObject(), @src());
        for (0..expr.data.e_object.properties.len) |i| {
            const prop = &expr.data.e_object.properties.ptr[i];
            const key = prop.key orelse continue;
            if (std.meta.activeTag(key.data) != .e_string) continue;
            if (key.data.e_string.eql(string, name)) {
                prop.value = value;
                return;
            }
        }

        var new_props = expr.data.e_object.properties.listManaged(allocator);
        try new_props.append(.{
            .key = Expr.init(E.String, .{ .data = name }, logger.Loc.Empty),
            .value = value,
        });

        expr.data.e_object.properties = BabyList(G.Property).fromList(new_props);
    }

    /// Don't use this if you care about performance.
    ///
    /// Sets the value of a property to a string, creating it if it doesn't exist.
    /// `expr` must be an object.
    pub fn setString(expr: *Expr, allocator: std.mem.Allocator, name: string, value: string) OOM!void {
        bun.assertWithLocation(expr.isObject(), @src());
        for (0..expr.data.e_object.properties.len) |i| {
            const prop = &expr.data.e_object.properties.ptr[i];
            const key = prop.key orelse continue;
            if (std.meta.activeTag(key.data) != .e_string) continue;
            if (key.data.e_string.eql(string, name)) {
                prop.value = Expr.init(E.String, .{ .data = value }, logger.Loc.Empty);
                return;
            }
        }

        var new_props = expr.data.e_object.properties.listManaged(allocator);
        try new_props.append(.{
            .key = Expr.init(E.String, .{ .data = name }, logger.Loc.Empty),
            .value = Expr.init(E.String, .{ .data = value }, logger.Loc.Empty),
        });

        expr.data.e_object.properties = BabyList(G.Property).fromList(new_props);
    }

    pub fn getObject(expr: *const Expr, name: string) ?Expr {
        if (expr.asProperty(name)) |query| {
            if (query.expr.isObject()) {
                return query.expr;
            }
        }
        return null;
    }

    pub fn getString(expr: *const Expr, allocator: std.mem.Allocator, name: string) OOM!?struct { string, logger.Loc } {
        if (asProperty(expr, name)) |q| {
            if (q.expr.asString(allocator)) |str| {
                return .{
                    str,
                    q.expr.loc,
                };
            }
        }
        return null;
    }

    pub fn getNumber(expr: *const Expr, name: string) ?struct { f64, logger.Loc } {
        if (asProperty(expr, name)) |q| {
            if (q.expr.asNumber()) |num| {
                return .{
                    num,
                    q.expr.loc,
                };
            }
        }
        return null;
    }

    pub fn getStringCloned(expr: *const Expr, allocator: std.mem.Allocator, name: string) OOM!?string {
        return if (asProperty(expr, name)) |q| q.expr.asStringCloned(allocator) else null;
    }

    pub fn getStringClonedZ(expr: *const Expr, allocator: std.mem.Allocator, name: string) OOM!?stringZ {
        return if (asProperty(expr, name)) |q| q.expr.asStringZ(allocator) else null;
    }

    pub fn getArray(expr: *const Expr, name: string) ?ArrayIterator {
        return if (asProperty(expr, name)) |q| q.expr.asArray() else null;
    }

    pub fn getRope(self: *const Expr, rope: *const E.Object.Rope) ?E.Object.RopeQuery {
        if (self.get(rope.head.data.e_string.data)) |existing| {
            switch (existing.data) {
                .e_array => |array| {
                    if (rope.next) |next| {
                        if (array.items.last()) |end| {
                            return end.getRope(next);
                        }
                    }

                    return E.Object.RopeQuery{
                        .expr = existing,
                        .rope = rope,
                    };
                },
                .e_object => {
                    if (rope.next) |next| {
                        if (existing.getRope(next)) |end| {
                            return end;
                        }
                    }

                    return E.Object.RopeQuery{
                        .expr = existing,
                        .rope = rope,
                    };
                },
                else => return E.Object.RopeQuery{
                    .expr = existing,
                    .rope = rope,
                },
            }
        }

        return null;
    }

    // Making this comptime bloats the binary and doesn't seem to impact runtime performance.
    pub fn asProperty(expr: *const Expr, name: string) ?Query {
        if (std.meta.activeTag(expr.data) != .e_object) return null;
        const obj = expr.data.e_object;
        if (obj.properties.len == 0) return null;

        return obj.asProperty(name);
    }

    pub fn asPropertyStringMap(expr: *const Expr, name: string, allocator: std.mem.Allocator) ?*bun.StringArrayHashMap(string) {
        if (std.meta.activeTag(expr.data) != .e_object) return null;
        const obj_ = expr.data.e_object;
        if (obj_.properties.len == 0) return null;
        const query = obj_.asProperty(name) orelse return null;
        if (query.expr.data != .e_object) return null;

        const obj = query.expr.data.e_object;
        var count: usize = 0;
        for (obj.properties.slice()) |prop| {
            const key = prop.key.?.asString(allocator) orelse continue;
            const value = prop.value.?.asString(allocator) orelse continue;
            count += @as(usize, @intFromBool(key.len > 0 and value.len > 0));
        }

        if (count == 0) return null;
        var map = bun.StringArrayHashMap(string).init(allocator);
        map.ensureUnusedCapacity(count) catch return null;

        for (obj.properties.slice()) |prop| {
            const key = prop.key.?.asString(allocator) orelse continue;
            const value = prop.value.?.asString(allocator) orelse continue;

            if (!(key.len > 0 and value.len > 0)) continue;

            map.putAssumeCapacity(key, value);
        }

        const ptr = allocator.create(bun.StringArrayHashMap(string)) catch unreachable;
        ptr.* = map;
        return ptr;
    }

    pub const ArrayIterator = struct {
        array: *const E.Array,
        index: u32,

        pub fn next(this: *ArrayIterator) ?Expr {
            if (this.index >= this.array.items.len) {
                return null;
            }
            defer this.index += 1;
            return this.array.items.ptr[this.index];
        }
    };

    pub fn asArray(expr: *const Expr) ?ArrayIterator {
        if (std.meta.activeTag(expr.data) != .e_array) return null;
        const array = expr.data.e_array;
        if (array.items.len == 0) return null;

        return ArrayIterator{ .array = array, .index = 0 };
    }

    pub inline fn asUtf8StringLiteral(expr: *const Expr) ?string {
        if (expr.data == .e_string) {
            bun.debugAssert(expr.data.e_string.next == null);
            return expr.data.e_string.data;
        }
        return null;
    }

    pub inline fn asStringLiteral(expr: *const Expr, allocator: std.mem.Allocator) ?string {
        if (std.meta.activeTag(expr.data) != .e_string) return null;
        return expr.data.e_string.string(allocator) catch null;
    }

    pub inline fn isString(expr: *const Expr) bool {
        return switch (expr.data) {
            .e_string => true,
            else => false,
        };
    }

    pub inline fn asString(expr: *const Expr, allocator: std.mem.Allocator) ?string {
        switch (expr.data) {
            .e_string => |str| return str.string(allocator) catch bun.outOfMemory(),
            else => return null,
        }
    }
    pub inline fn asStringHash(expr: *const Expr, allocator: std.mem.Allocator, comptime hash_fn: *const fn (buf: []const u8) callconv(.Inline) u64) OOM!?u64 {
        switch (expr.data) {
            .e_string => |str| {
                if (str.isUTF8()) return hash_fn(str.data);
                const utf8_str = try str.string(allocator);
                defer allocator.free(utf8_str);
                return hash_fn(utf8_str);
            },
            else => return null,
        }
    }

    pub inline fn asStringCloned(expr: *const Expr, allocator: std.mem.Allocator) OOM!?string {
        switch (expr.data) {
            .e_string => |str| return try str.stringCloned(allocator),
            else => return null,
        }
    }

    pub inline fn asStringZ(expr: *const Expr, allocator: std.mem.Allocator) OOM!?stringZ {
        switch (expr.data) {
            .e_string => |str| return try str.stringZ(allocator),
            else => return null,
        }
    }

    pub fn asBool(
        expr: *const Expr,
    ) ?bool {
        if (std.meta.activeTag(expr.data) != .e_boolean) return null;

        return expr.data.e_boolean.value;
    }

    pub fn asNumber(expr: *const Expr) ?f64 {
        if (expr.data != .e_number) return null;

        return expr.data.e_number.value;
    }

    pub const EFlags = enum { none, ts_decorator };

    const Serializable = struct {
        type: Tag,
        object: string,
        value: Data,
        loc: logger.Loc,
    };

    pub fn isMissing(a: *const Expr) bool {
        return std.meta.activeTag(a.data) == Expr.Tag.e_missing;
    }

    // The goal of this function is to "rotate" the AST if it's possible to use the
    // left-associative property of the operator to avoid unnecessary parentheses.
    //
    // When using this, make absolutely sure that the operator is actually
    // associative. For example, the "-" operator is not associative for
    // floating-point numbers.
    pub fn joinWithLeftAssociativeOp(
        comptime op: Op.Code,
        a: Expr,
        b: Expr,
        allocator: std.mem.Allocator,
    ) Expr {
        // "(a, b) op c" => "a, b op c"
        switch (a.data) {
            .e_binary => |comma| {
                if (comma.op == .bin_comma) {
                    comma.right = joinWithLeftAssociativeOp(op, comma.right, b, allocator);
                }
            },
            else => {},
        }

        // "a op (b op c)" => "(a op b) op c"
        // "a op (b op (c op d))" => "((a op b) op c) op d"
        switch (b.data) {
            .e_binary => |binary| {
                if (binary.op == op) {
                    return joinWithLeftAssociativeOp(
                        op,
                        joinWithLeftAssociativeOp(op, a, binary.left, allocator),
                        binary.right,
                        allocator,
                    );
                }
            },
            else => {},
        }

        // "a op b" => "a op b"
        // "(a op b) op c" => "(a op b) op c"
        return Expr.init(E.Binary, E.Binary{ .op = op, .left = a, .right = b }, a.loc);
    }

    pub fn joinWithComma(a: Expr, b: Expr, _: std.mem.Allocator) Expr {
        if (a.isMissing()) {
            return b;
        }

        if (b.isMissing()) {
            return a;
        }

        return Expr.init(E.Binary, E.Binary{ .op = .bin_comma, .left = a, .right = b }, a.loc);
    }

    pub fn joinAllWithComma(all: []Expr, allocator: std.mem.Allocator) Expr {
        bun.assert(all.len > 0);
        switch (all.len) {
            1 => {
                return all[0];
            },
            2 => {
                return Expr.joinWithComma(all[0], all[1], allocator);
            },
            else => {
                var expr = all[0];
                for (1..all.len) |i| {
                    expr = Expr.joinWithComma(expr, all[i], allocator);
                }
                return expr;
            },
        }
    }

    pub fn joinAllWithCommaCallback(all: []Expr, comptime Context: type, ctx: Context, comptime callback: (fn (ctx: anytype, expr: Expr) ?Expr), allocator: std.mem.Allocator) ?Expr {
        switch (all.len) {
            0 => return null,
            1 => {
                return callback(ctx, all[0]);
            },
            2 => {
                return Expr.joinWithComma(
                    callback(ctx, all[0]) orelse Expr{
                        .data = .{ .e_missing = .{} },
                        .loc = all[0].loc,
                    },
                    callback(ctx, all[1]) orelse Expr{
                        .data = .{ .e_missing = .{} },
                        .loc = all[1].loc,
                    },
                    allocator,
                );
            },
            else => {
                var i: usize = 1;
                var expr = callback(ctx, all[0]) orelse Expr{
                    .data = .{ .e_missing = .{} },
                    .loc = all[0].loc,
                };

                while (i < all.len) : (i += 1) {
                    expr = Expr.joinWithComma(expr, callback(ctx, all[i]) orelse Expr{
                        .data = .{ .e_missing = .{} },
                        .loc = all[i].loc,
                    }, allocator);
                }

                return expr;
            },
        }
    }

    pub fn jsonStringify(self: *const @This(), writer: anytype) !void {
        return try writer.write(Serializable{ .type = std.meta.activeTag(self.data), .object = "expr", .value = self.data, .loc = self.loc });
    }

    pub fn extractNumericValues(left: Expr.Data, right: Expr.Data) ?[2]f64 {
        return .{
            left.extractNumericValue() orelse return null,
            right.extractNumericValue() orelse return null,
        };
    }

    pub var icount: usize = 0;

    // We don't need to dynamically allocate booleans
    var true_bool = E.Boolean{ .value = true };
    var false_bool = E.Boolean{ .value = false };
    var bool_values = [_]*E.Boolean{ &false_bool, &true_bool };

    /// When the lifetime of an Expr.Data's pointer must exist longer than reset() is called, use this function.
    /// Be careful to free the memory (or use an allocator that does it for you)
    /// Also, prefer Expr.init or Expr.alloc when possible. This will be slower.
    pub fn allocate(allocator: std.mem.Allocator, comptime Type: type, st: Type, loc: logger.Loc) Expr {
        icount += 1;
        Data.Store.assert();

        switch (Type) {
            E.Array => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_array = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.Class => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_class = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.Unary => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_unary = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.Binary => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_binary = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.This => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_this = st,
                    },
                };
            },
            E.Boolean => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_boolean = st,
                    },
                };
            },
            E.Super => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_super = st,
                    },
                };
            },
            E.Null => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_null = st,
                    },
                };
            },
            E.Undefined => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_undefined = st,
                    },
                };
            },
            E.New => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_new = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.NewTarget => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_new_target = st,
                    },
                };
            },
            E.Function => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_function = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.ImportMeta => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_import_meta = st,
                    },
                };
            },
            E.Call => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_call = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.Dot => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_dot = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.Index => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_index = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.Arrow => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_arrow = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.Identifier => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_identifier = E.Identifier{
                            .ref = st.ref,
                            .must_keep_due_to_with_stmt = st.must_keep_due_to_with_stmt,
                            .can_be_removed_if_unused = st.can_be_removed_if_unused,
                            .call_can_be_unwrapped_if_unused = st.call_can_be_unwrapped_if_unused,
                        },
                    },
                };
            },
            E.ImportIdentifier => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_import_identifier = .{
                            .ref = st.ref,
                            .was_originally_identifier = st.was_originally_identifier,
                        },
                    },
                };
            },
            E.CommonJSExportIdentifier => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_commonjs_export_identifier = .{
                            .ref = st.ref,
                        },
                    },
                };
            },

            E.PrivateIdentifier => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_private_identifier = st,
                    },
                };
            },
            E.JSXElement => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_jsx_element = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.Missing => {
                return Expr{ .loc = loc, .data = Data{ .e_missing = E.Missing{} } };
            },
            E.Number => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_number = st,
                    },
                };
            },
            E.BigInt => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_big_int = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.Object => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_object = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.Spread => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_spread = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.String => {
                if (comptime Environment.isDebug) {
                    // Sanity check: assert string is not a null ptr
                    if (st.data.len > 0 and st.isUTF8()) {
                        bun.assert(@intFromPtr(st.data.ptr) > 0);
                    }
                }
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_string = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },

            E.Template => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_template = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.RegExp => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_reg_exp = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.Await => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_await = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.Yield => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_yield = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.If => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_if = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.RequireResolveString => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_require_resolve_string = st,
                    },
                };
            },
            E.Import => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_import = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st;
                            break :brk item;
                        },
                    },
                };
            },
            E.RequireString => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_require_string = st,
                    },
                };
            },
            *E.String => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_string = brk: {
                            const item = allocator.create(Type) catch unreachable;
                            item.* = st.*;
                            break :brk item;
                        },
                    },
                };
            },

            else => {
                @compileError("Invalid type passed to Expr.init: " ++ @typeName(Type));
            },
        }
    }

    pub const Disabler = bun.DebugOnlyDisabler(@This());

    pub fn init(comptime Type: type, st: Type, loc: logger.Loc) Expr {
        icount += 1;
        Data.Store.assert();

        switch (Type) {
            E.NameOfSymbol => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_name_of_symbol = Data.Store.append(E.NameOfSymbol, st),
                    },
                };
            },
            E.Array => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_array = Data.Store.append(Type, st),
                    },
                };
            },
            E.Class => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_class = Data.Store.append(Type, st),
                    },
                };
            },
            E.Unary => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_unary = Data.Store.append(Type, st),
                    },
                };
            },
            E.Binary => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_binary = Data.Store.append(Type, st),
                    },
                };
            },
            E.This => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_this = st,
                    },
                };
            },
            E.Boolean => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_boolean = st,
                    },
                };
            },
            E.Super => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_super = st,
                    },
                };
            },
            E.Null => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_null = st,
                    },
                };
            },
            E.Undefined => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_undefined = st,
                    },
                };
            },
            E.New => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_new = Data.Store.append(Type, st),
                    },
                };
            },
            E.NewTarget => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_new_target = st,
                    },
                };
            },
            E.Function => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_function = Data.Store.append(Type, st),
                    },
                };
            },
            E.ImportMeta => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_import_meta = st,
                    },
                };
            },
            E.Call => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_call = Data.Store.append(Type, st),
                    },
                };
            },
            E.Dot => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_dot = Data.Store.append(Type, st),
                    },
                };
            },
            E.Index => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_index = Data.Store.append(Type, st),
                    },
                };
            },
            E.Arrow => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_arrow = Data.Store.append(Type, st),
                    },
                };
            },
            E.Identifier => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_identifier = E.Identifier{
                            .ref = st.ref,
                            .must_keep_due_to_with_stmt = st.must_keep_due_to_with_stmt,
                            .can_be_removed_if_unused = st.can_be_removed_if_unused,
                            .call_can_be_unwrapped_if_unused = st.call_can_be_unwrapped_if_unused,
                        },
                    },
                };
            },
            E.ImportIdentifier => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_import_identifier = .{
                            .ref = st.ref,
                            .was_originally_identifier = st.was_originally_identifier,
                        },
                    },
                };
            },
            E.CommonJSExportIdentifier => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_commonjs_export_identifier = .{
                            .ref = st.ref,
                            .base = st.base,
                        },
                    },
                };
            },
            E.PrivateIdentifier => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_private_identifier = st,
                    },
                };
            },
            E.JSXElement => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_jsx_element = Data.Store.append(Type, st),
                    },
                };
            },
            E.Missing => {
                return Expr{ .loc = loc, .data = Data{ .e_missing = E.Missing{} } };
            },
            E.Number => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_number = st,
                    },
                };
            },
            E.BigInt => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_big_int = Data.Store.append(Type, st),
                    },
                };
            },
            E.Object => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_object = Data.Store.append(Type, st),
                    },
                };
            },
            E.Spread => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_spread = Data.Store.append(Type, st),
                    },
                };
            },
            E.String => {
                if (comptime Environment.isDebug) {
                    // Sanity check: assert string is not a null ptr
                    if (st.data.len > 0 and st.isUTF8()) {
                        bun.assert(@intFromPtr(st.data.ptr) > 0);
                    }
                }
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_string = Data.Store.append(Type, st),
                    },
                };
            },

            E.Template => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_template = Data.Store.append(Type, st),
                    },
                };
            },
            E.RegExp => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_reg_exp = Data.Store.append(Type, st),
                    },
                };
            },
            E.Await => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_await = Data.Store.append(Type, st),
                    },
                };
            },
            E.Yield => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_yield = Data.Store.append(Type, st),
                    },
                };
            },
            E.If => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_if = Data.Store.append(Type, st),
                    },
                };
            },
            E.RequireResolveString => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_require_resolve_string = st,
                    },
                };
            },
            E.Import => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_import = Data.Store.append(Type, st),
                    },
                };
            },
            E.RequireString => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_require_string = st,
                    },
                };
            },
            *E.String => {
                return Expr{
                    .loc = loc,
                    .data = Data{
                        .e_string = Data.Store.append(@TypeOf(st.*), st.*),
                    },
                };
            },
            E.InlinedEnum => return .{ .loc = loc, .data = .{
                .e_inlined_enum = Data.Store.append(@TypeOf(st), st),
            } },

            else => {
                @compileError("Invalid type passed to Expr.init: " ++ @typeName(Type));
            },
        }
    }

    pub fn isPrimitiveLiteral(this: Expr) bool {
        return @as(Tag, this.data).isPrimitiveLiteral();
    }

    pub fn isRef(this: Expr, ref: Ref) bool {
        return switch (this.data) {
            .e_import_identifier => |import_identifier| import_identifier.ref.eql(ref),
            .e_identifier => |ident| ident.ref.eql(ref),
            else => false,
        };
    }

    pub const Tag = enum {
        e_array,
        e_unary,
        e_binary,
        e_class,
        e_new,
        e_function,
        e_call,
        e_dot,
        e_index,
        e_arrow,
        e_jsx_element,
        e_object,
        e_spread,
        e_template,
        e_reg_exp,
        e_await,
        e_yield,
        e_if,
        e_import,
        e_identifier,
        e_import_identifier,
        e_private_identifier,
        e_commonjs_export_identifier,
        e_boolean,
        e_number,
        e_big_int,
        e_string,
        e_require_string,
        e_require_resolve_string,
        e_require_call_target,
        e_require_resolve_call_target,
        e_missing,
        e_this,
        e_super,
        e_null,
        e_undefined,
        e_new_target,
        e_import_meta,
        e_import_meta_main,
        e_require_main,
        e_special,
        e_inlined_enum,
        e_name_of_symbol,

        // object, regex and array may have had side effects
        pub fn isPrimitiveLiteral(tag: Tag) bool {
            return switch (tag) {
                .e_null, .e_undefined, .e_string, .e_boolean, .e_number, .e_big_int => true,
                else => false,
            };
        }

        pub fn typeof(tag: Tag) ?string {
            return switch (tag) {
                .e_array, .e_object, .e_null, .e_reg_exp => "object",
                .e_undefined => "undefined",
                .e_boolean => "boolean",
                .e_number => "number",
                .e_big_int => "bigint",
                .e_string => "string",
                .e_class, .e_function, .e_arrow => "function",
                else => null,
            };
        }

        pub fn format(tag: Tag, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
            try switch (tag) {
                .e_string => writer.writeAll("string"),
                .e_array => writer.writeAll("array"),
                .e_unary => writer.writeAll("unary"),
                .e_binary => writer.writeAll("binary"),
                .e_boolean => writer.writeAll("boolean"),
                .e_super => writer.writeAll("super"),
                .e_null => writer.writeAll("null"),
                .e_undefined => writer.writeAll("undefined"),
                .e_new => writer.writeAll("new"),
                .e_function => writer.writeAll("function"),
                .e_new_target => writer.writeAll("new target"),
                .e_import_meta => writer.writeAll("import.meta"),
                .e_call => writer.writeAll("call"),
                .e_dot => writer.writeAll("dot"),
                .e_index => writer.writeAll("index"),
                .e_arrow => writer.writeAll("arrow"),
                .e_identifier => writer.writeAll("identifier"),
                .e_import_identifier => writer.writeAll("import identifier"),
                .e_private_identifier => writer.writeAll("#privateIdentifier"),
                .e_jsx_element => writer.writeAll("<jsx>"),
                .e_missing => writer.writeAll("<missing>"),
                .e_number => writer.writeAll("number"),
                .e_big_int => writer.writeAll("BigInt"),
                .e_object => writer.writeAll("object"),
                .e_spread => writer.writeAll("..."),
                .e_template => writer.writeAll("template"),
                .e_reg_exp => writer.writeAll("regexp"),
                .e_await => writer.writeAll("await"),
                .e_yield => writer.writeAll("yield"),
                .e_if => writer.writeAll("if"),
                .e_require_resolve_string => writer.writeAll("require_or_require_resolve"),
                .e_import => writer.writeAll("import"),
                .e_this => writer.writeAll("this"),
                .e_class => writer.writeAll("class"),
                .e_require_string => writer.writeAll("require"),
                else => writer.writeAll(@tagName(tag)),
            };
        }

        pub fn jsonStringify(self: @This(), writer: anytype) !void {
            return try writer.write(@tagName(self));
        }

        pub fn isArray(self: Tag) bool {
            switch (self) {
                .e_array => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isUnary(self: Tag) bool {
            switch (self) {
                .e_unary => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isBinary(self: Tag) bool {
            switch (self) {
                .e_binary => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isThis(self: Tag) bool {
            switch (self) {
                .e_this => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isClass(self: Tag) bool {
            switch (self) {
                .e_class => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isBoolean(self: Tag) bool {
            switch (self) {
                .e_boolean => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isSuper(self: Tag) bool {
            switch (self) {
                .e_super => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isNull(self: Tag) bool {
            switch (self) {
                .e_null => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isUndefined(self: Tag) bool {
            switch (self) {
                .e_undefined => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isNew(self: Tag) bool {
            switch (self) {
                .e_new => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isNewTarget(self: Tag) bool {
            switch (self) {
                .e_new_target => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isFunction(self: Tag) bool {
            switch (self) {
                .e_function => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isImportMeta(self: Tag) bool {
            switch (self) {
                .e_import_meta => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isCall(self: Tag) bool {
            switch (self) {
                .e_call => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isDot(self: Tag) bool {
            switch (self) {
                .e_dot => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isIndex(self: Tag) bool {
            switch (self) {
                .e_index => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isArrow(self: Tag) bool {
            switch (self) {
                .e_arrow => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isIdentifier(self: Tag) bool {
            switch (self) {
                .e_identifier => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isImportIdentifier(self: Tag) bool {
            switch (self) {
                .e_import_identifier => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isPrivateIdentifier(self: Tag) bool {
            switch (self) {
                .e_private_identifier => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isJsxElement(self: Tag) bool {
            switch (self) {
                .e_jsx_element => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isMissing(self: Tag) bool {
            switch (self) {
                .e_missing => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isNumber(self: Tag) bool {
            switch (self) {
                .e_number => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isBigInt(self: Tag) bool {
            switch (self) {
                .e_big_int => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isObject(self: Tag) bool {
            switch (self) {
                .e_object => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isSpread(self: Tag) bool {
            switch (self) {
                .e_spread => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isString(self: Tag) bool {
            switch (self) {
                .e_string => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }

        pub fn isTemplate(self: Tag) bool {
            switch (self) {
                .e_template => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isRegExp(self: Tag) bool {
            switch (self) {
                .e_reg_exp => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isAwait(self: Tag) bool {
            switch (self) {
                .e_await => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isYield(self: Tag) bool {
            switch (self) {
                .e_yield => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isIf(self: Tag) bool {
            switch (self) {
                .e_if => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isRequireResolveString(self: Tag) bool {
            switch (self) {
                .e_require_resolve_string => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
        pub fn isImport(self: Tag) bool {
            switch (self) {
                .e_import => {
                    return true;
                },
                else => {
                    return false;
                },
            }
        }
    };

    pub fn isBoolean(a: Expr) bool {
        switch (a.data) {
            .e_boolean => {
                return true;
            },

            .e_if => |ex| {
                return isBoolean(ex.yes) and isBoolean(ex.no);
            },
            .e_unary => |ex| {
                return ex.op == .un_not or ex.op == .un_delete;
            },
            .e_binary => |ex| {
                switch (ex.op) {
                    .bin_strict_eq, .bin_strict_ne, .bin_loose_eq, .bin_loose_ne, .bin_lt, .bin_gt, .bin_le, .bin_ge, .bin_instanceof, .bin_in => {
                        return true;
                    },
                    .bin_logical_or => {
                        return isBoolean(ex.left) and isBoolean(ex.right);
                    },
                    .bin_logical_and => {
                        return isBoolean(ex.left) and isBoolean(ex.right);
                    },
                    else => {},
                }
            },
            else => {},
        }

        return false;
    }

    pub fn assign(a: Expr, b: Expr) Expr {
        return init(E.Binary, E.Binary{
            .op = .bin_assign,
            .left = a,
            .right = b,
        }, a.loc);
    }
    pub inline fn at(expr: Expr, comptime Type: type, t: Type, _: std.mem.Allocator) Expr {
        return init(Type, t, expr.loc);
    }

    // Wraps the provided expression in the "!" prefix operator. The expression
    // will potentially be simplified to avoid generating unnecessary extra "!"
    // operators. For example, calling this with "!!x" will return "!x" instead
    // of returning "!!!x".
    pub fn not(expr: Expr, allocator: std.mem.Allocator) Expr {
        return maybeSimplifyNot(
            expr,
            allocator,
        ) orelse Expr.init(
            E.Unary,
            E.Unary{
                .op = .un_not,
                .value = expr,
            },
            expr.loc,
        );
    }

    pub fn hasValueForThisInCall(expr: Expr) bool {
        return switch (expr.data) {
            .e_dot, .e_index => true,
            else => false,
        };
    }

    /// The given "expr" argument should be the operand of a "!" prefix operator
    /// (i.e. the "x" in "!x"). This returns a simplified expression for the
    /// whole operator (i.e. the "!x") if it can be simplified, or false if not.
    /// It's separate from "Not()" above to avoid allocation on failure in case
    /// that is undesired.
    pub fn maybeSimplifyNot(expr: Expr, allocator: std.mem.Allocator) ?Expr {
        switch (expr.data) {
            .e_null, .e_undefined => {
                return expr.at(E.Boolean, E.Boolean{ .value = true }, allocator);
            },
            .e_boolean => |b| {
                return expr.at(E.Boolean, E.Boolean{ .value = b.value }, allocator);
            },
            .e_number => |n| {
                return expr.at(E.Boolean, E.Boolean{ .value = (n.value == 0 or std.math.isNan(n.value)) }, allocator);
            },
            .e_big_int => |b| {
                return expr.at(E.Boolean, E.Boolean{ .value = strings.eqlComptime(b.value, "0") }, allocator);
            },
            .e_function,
            .e_arrow,
            .e_reg_exp,
            => {
                return expr.at(E.Boolean, E.Boolean{ .value = false }, allocator);
            },
            // "!!!a" => "!a"
            .e_unary => |un| {
                if (un.op == Op.Code.un_not and knownPrimitive(un.value) == .boolean) {
                    return un.value;
                }
            },
            .e_binary => |ex| {
                // TODO: evaluate whether or not it is safe to do this mutation since it's modifying in-place.
                // Make sure that these transformations are all safe for special values.
                // For example, "!(a < b)" is not the same as "a >= b" if a and/or b are
                // NaN (or undefined, or null, or possibly other problem cases too).
                switch (ex.op) {
                    Op.Code.bin_loose_eq => {
                        // "!(a == b)" => "a != b"
                        ex.op = .bin_loose_ne;
                        return expr;
                    },
                    Op.Code.bin_loose_ne => {
                        // "!(a != b)" => "a == b"
                        ex.op = .bin_loose_eq;
                        return expr;
                    },
                    Op.Code.bin_strict_eq => {
                        // "!(a === b)" => "a !== b"
                        ex.op = .bin_strict_ne;
                        return expr;
                    },
                    Op.Code.bin_strict_ne => {
                        // "!(a !== b)" => "a === b"
                        ex.op = .bin_strict_eq;
                        return expr;
                    },
                    Op.Code.bin_comma => {
                        // "!(a, b)" => "a, !b"
                        ex.right = ex.right.not(allocator);
                        return expr;
                    },
                    else => {},
                }
            },
            .e_inlined_enum => |inlined| {
                return maybeSimplifyNot(inlined.value, allocator);
            },

            else => {},
        }

        return null;
    }

    pub fn toStringExprWithoutSideEffects(expr: Expr, allocator: std.mem.Allocator) ?Expr {
        const unwrapped = expr.unwrapInlined();
        const slice = switch (unwrapped.data) {
            .e_null => "null",
            .e_string => return expr,
            .e_undefined => "undefined",
            .e_boolean => |data| if (data.value) "true" else "false",
            .e_big_int => |bigint| bigint.value,
            .e_number => |num| if (num.toString(allocator)) |str|
                str
            else
                null,
            .e_reg_exp => |regexp| regexp.value,
            .e_dot => |dot| @as(?[]const u8, brk: {
                // This is dumb but some JavaScript obfuscators use this to generate string literals
                if (bun.strings.eqlComptime(dot.name, "constructor")) {
                    break :brk switch (dot.target.data) {
                        .e_string => "function String() { [native code] }",
                        .e_reg_exp => "function RegExp() { [native code] }",
                        else => null,
                    };
                }
                break :brk null;
            }),
            else => null,
        };
        return if (slice) |s| Expr.init(E.String, E.String.init(s), expr.loc) else null;
    }

    pub fn isOptionalChain(self: *const @This()) bool {
        return switch (self.data) {
            .e_dot => self.data.e_dot.optional_chain != null,
            .e_index => self.data.e_index.optional_chain != null,
            .e_call => self.data.e_call.optional_chain != null,
            else => false,
        };
    }

    pub inline fn knownPrimitive(self: @This()) PrimitiveType {
        return self.data.knownPrimitive();
    }

    pub const PrimitiveType = enum {
        unknown,
        mixed,
        null,
        undefined,
        boolean,
        number,
        string,
        bigint,

        pub const static = std.enums.EnumSet(PrimitiveType).init(.{
            .mixed = true,
            .null = true,
            .undefined = true,
            .boolean = true,
            .number = true,
            .string = true,
            // for our purposes, bigint is dynamic
            // it is technically static though
            // .@"bigint" = true,
        });

        pub inline fn isStatic(this: PrimitiveType) bool {
            return static.contains(this);
        }

        pub fn merge(left_known: PrimitiveType, right_known: PrimitiveType) PrimitiveType {
            if (right_known == .unknown or left_known == .unknown)
                return .unknown;

            return if (left_known == right_known)
                left_known
            else
                .mixed;
        }
    };

    pub const Data = union(Tag) {
        e_array: *E.Array,
        e_unary: *E.Unary,
        e_binary: *E.Binary,
        e_class: *E.Class,

        e_new: *E.New,
        e_function: *E.Function,
        e_call: *E.Call,
        e_dot: *E.Dot,
        e_index: *E.Index,
        e_arrow: *E.Arrow,

        e_jsx_element: *E.JSXElement,
        e_object: *E.Object,
        e_spread: *E.Spread,
        e_template: *E.Template,
        e_reg_exp: *E.RegExp,
        e_await: *E.Await,
        e_yield: *E.Yield,
        e_if: *E.If,
        e_import: *E.Import,

        e_identifier: E.Identifier,
        e_import_identifier: E.ImportIdentifier,
        e_private_identifier: E.PrivateIdentifier,
        e_commonjs_export_identifier: E.CommonJSExportIdentifier,

        e_boolean: E.Boolean,
        e_number: E.Number,
        e_big_int: *E.BigInt,
        e_string: *E.String,

        e_require_string: E.RequireString,
        e_require_resolve_string: E.RequireResolveString,
        e_require_call_target,
        e_require_resolve_call_target,

        e_missing: E.Missing,
        e_this: E.This,
        e_super: E.Super,
        e_null: E.Null,
        e_undefined: E.Undefined,
        e_new_target: E.NewTarget,
        e_import_meta: E.ImportMeta,

        e_import_meta_main: E.ImportMetaMain,
        e_require_main,

        /// Covers some exotic AST node types under one namespace, since the
        /// places this is found it all follows similar handling.
        e_special: E.Special,

        e_inlined_enum: *E.InlinedEnum,

        e_name_of_symbol: *E.NameOfSymbol,

        comptime {
            bun.assert_eql(@sizeOf(Data), 24); // Do not increase the size of Expr
        }

        pub fn as(data: Data, comptime tag: Tag) ?@FieldType(Data, @tagName(tag)) {
            return if (data == tag) @field(data, @tagName(tag)) else null;
        }

        pub fn clone(this: Expr.Data, allocator: std.mem.Allocator) !Data {
            return switch (this) {
                .e_array => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_array)));
                    item.* = el.*;
                    return .{ .e_array = item };
                },
                .e_unary => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_unary)));
                    item.* = el.*;
                    return .{ .e_unary = item };
                },
                .e_binary => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_binary)));
                    item.* = el.*;
                    return .{ .e_binary = item };
                },
                .e_class => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_class)));
                    item.* = el.*;
                    return .{ .e_class = item };
                },
                .e_new => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_new)));
                    item.* = el.*;
                    return .{ .e_new = item };
                },
                .e_function => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_function)));
                    item.* = el.*;
                    return .{ .e_function = item };
                },
                .e_call => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_call)));
                    item.* = el.*;
                    return .{ .e_call = item };
                },
                .e_dot => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_dot)));
                    item.* = el.*;
                    return .{ .e_dot = item };
                },
                .e_index => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_index)));
                    item.* = el.*;
                    return .{ .e_index = item };
                },
                .e_arrow => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_arrow)));
                    item.* = el.*;
                    return .{ .e_arrow = item };
                },
                .e_jsx_element => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_jsx_element)));
                    item.* = el.*;
                    return .{ .e_jsx_element = item };
                },
                .e_object => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_object)));
                    item.* = el.*;
                    return .{ .e_object = item };
                },
                .e_spread => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_spread)));
                    item.* = el.*;
                    return .{ .e_spread = item };
                },
                .e_template => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_template)));
                    item.* = el.*;
                    return .{ .e_template = item };
                },
                .e_reg_exp => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_reg_exp)));
                    item.* = el.*;
                    return .{ .e_reg_exp = item };
                },
                .e_await => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_await)));
                    item.* = el.*;
                    return .{ .e_await = item };
                },
                .e_yield => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_yield)));
                    item.* = el.*;
                    return .{ .e_yield = item };
                },
                .e_if => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_if)));
                    item.* = el.*;
                    return .{ .e_if = item };
                },
                .e_import => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_import)));
                    item.* = el.*;
                    return .{ .e_import = item };
                },
                .e_big_int => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_big_int)));
                    item.* = el.*;
                    return .{ .e_big_int = item };
                },
                .e_string => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_string)));
                    item.* = el.*;
                    return .{ .e_string = item };
                },
                .e_inlined_enum => |el| {
                    const item = try allocator.create(std.meta.Child(@TypeOf(this.e_inlined_enum)));
                    item.* = el.*;
                    return .{ .e_inlined_enum = item };
                },
                else => this,
            };
        }

        pub fn deepClone(this: Expr.Data, allocator: std.mem.Allocator) !Data {
            return switch (this) {
                .e_array => |el| {
                    const items = try el.items.deepClone(allocator);
                    const item = bun.create(allocator, E.Array, .{
                        .items = items,
                        .comma_after_spread = el.comma_after_spread,
                        .was_originally_macro = el.was_originally_macro,
                        .is_single_line = el.is_single_line,
                        .is_parenthesized = el.is_parenthesized,
                        .close_bracket_loc = el.close_bracket_loc,
                    });
                    return .{ .e_array = item };
                },
                .e_unary => |el| {
                    const item = bun.create(allocator, E.Unary, .{
                        .op = el.op,
                        .value = try el.value.deepClone(allocator),
                    });
                    return .{ .e_unary = item };
                },
                .e_binary => |el| {
                    const item = bun.create(allocator, E.Binary, .{
                        .op = el.op,
                        .left = try el.left.deepClone(allocator),
                        .right = try el.right.deepClone(allocator),
                    });
                    return .{ .e_binary = item };
                },
                .e_class => |el| {
                    const properties = try allocator.alloc(G.Property, el.properties.len);
                    for (el.properties, 0..) |prop, i| {
                        properties[i] = try prop.deepClone(allocator);
                    }

                    const item = bun.create(allocator, E.Class, .{
                        .class_keyword = el.class_keyword,
                        .ts_decorators = try el.ts_decorators.deepClone(allocator),
                        .class_name = el.class_name,
                        .extends = if (el.extends) |e| try e.deepClone(allocator) else null,
                        .body_loc = el.body_loc,
                        .close_brace_loc = el.close_brace_loc,
                        .properties = properties,
                        .has_decorators = el.has_decorators,
                    });
                    return .{ .e_class = item };
                },
                .e_new => |el| {
                    const item = bun.create(allocator, E.New, .{
                        .target = try el.target.deepClone(allocator),
                        .args = try el.args.deepClone(allocator),
                        .can_be_unwrapped_if_unused = el.can_be_unwrapped_if_unused,
                        .close_parens_loc = el.close_parens_loc,
                    });

                    return .{ .e_new = item };
                },
                .e_function => |el| {
                    const item = bun.create(allocator, E.Function, .{
                        .func = try el.func.deepClone(allocator),
                    });
                    return .{ .e_function = item };
                },
                .e_call => |el| {
                    const item = bun.create(allocator, E.Call, .{
                        .target = try el.target.deepClone(allocator),
                        .args = try el.args.deepClone(allocator),
                        .optional_chain = el.optional_chain,
                        .is_direct_eval = el.is_direct_eval,
                        .close_paren_loc = el.close_paren_loc,
                        .can_be_unwrapped_if_unused = el.can_be_unwrapped_if_unused,
                        .was_jsx_element = el.was_jsx_element,
                    });
                    return .{ .e_call = item };
                },
                .e_dot => |el| {
                    const item = bun.create(allocator, E.Dot, .{
                        .target = try el.target.deepClone(allocator),
                        .name = el.name,
                        .name_loc = el.name_loc,
                        .optional_chain = el.optional_chain,
                        .can_be_removed_if_unused = el.can_be_removed_if_unused,
                        .call_can_be_unwrapped_if_unused = el.call_can_be_unwrapped_if_unused,
                    });
                    return .{ .e_dot = item };
                },
                .e_index => |el| {
                    const item = bun.create(allocator, E.Index, .{
                        .target = try el.target.deepClone(allocator),
                        .index = try el.index.deepClone(allocator),
                        .optional_chain = el.optional_chain,
                    });
                    return .{ .e_index = item };
                },
                .e_arrow => |el| {
                    const args = try allocator.alloc(G.Arg, el.args.len);
                    for (0..args.len) |i| {
                        args[i] = try el.args[i].deepClone(allocator);
                    }
                    const item = bun.create(allocator, E.Arrow, .{
                        .args = args,
                        .body = el.body,
                        .is_async = el.is_async,
                        .has_rest_arg = el.has_rest_arg,
                        .prefer_expr = el.prefer_expr,
                    });

                    return .{ .e_arrow = item };
                },
                .e_jsx_element => |el| {
                    const item = bun.create(allocator, E.JSXElement, .{
                        .tag = if (el.tag) |tag| try tag.deepClone(allocator) else null,
                        .properties = try el.properties.deepClone(allocator),
                        .children = try el.children.deepClone(allocator),
                        .key_prop_index = el.key_prop_index,
                        .flags = el.flags,
                        .close_tag_loc = el.close_tag_loc,
                    });
                    return .{ .e_jsx_element = item };
                },
                .e_object => |el| {
                    const item = bun.create(allocator, E.Object, .{
                        .properties = try el.properties.deepClone(allocator),
                        .comma_after_spread = el.comma_after_spread,
                        .is_single_line = el.is_single_line,
                        .is_parenthesized = el.is_parenthesized,
                        .was_originally_macro = el.was_originally_macro,
                        .close_brace_loc = el.close_brace_loc,
                    });
                    return .{ .e_object = item };
                },
                .e_spread => |el| {
                    const item = bun.create(allocator, E.Spread, .{
                        .value = try el.value.deepClone(allocator),
                    });
                    return .{ .e_spread = item };
                },
                .e_template => |el| {
                    const item = bun.create(allocator, E.Template, .{
                        .tag = if (el.tag) |tag| try tag.deepClone(allocator) else null,
                        .parts = el.parts,
                        .head = el.head,
                    });
                    return .{ .e_template = item };
                },
                .e_reg_exp => |el| {
                    const item = bun.create(allocator, E.RegExp, .{
                        .value = el.value,
                        .flags_offset = el.flags_offset,
                    });
                    return .{ .e_reg_exp = item };
                },
                .e_await => |el| {
                    const item = bun.create(allocator, E.Await, .{
                        .value = try el.value.deepClone(allocator),
                    });
                    return .{ .e_await = item };
                },
                .e_yield => |el| {
                    const item = bun.create(allocator, E.Yield, .{
                        .value = if (el.value) |value| try value.deepClone(allocator) else null,
                        .is_star = el.is_star,
                    });
                    return .{ .e_yield = item };
                },
                .e_if => |el| {
                    const item = bun.create(allocator, E.If, .{
                        .test_ = try el.test_.deepClone(allocator),
                        .yes = try el.yes.deepClone(allocator),
                        .no = try el.no.deepClone(allocator),
                    });
                    return .{ .e_if = item };
                },
                .e_import => |el| {
                    const item = bun.create(allocator, E.Import, .{
                        .expr = try el.expr.deepClone(allocator),
                        .options = try el.options.deepClone(allocator),
                        .import_record_index = el.import_record_index,
                    });
                    return .{ .e_import = item };
                },
                .e_big_int => |el| {
                    const item = bun.create(allocator, E.BigInt, .{
                        .value = el.value,
                    });
                    return .{ .e_big_int = item };
                },
                .e_string => |el| {
                    const item = bun.create(allocator, E.String, .{
                        .data = el.data,
                        .prefer_template = el.prefer_template,
                        .next = el.next,
                        .end = el.end,
                        .rope_len = el.rope_len,
                        .is_utf16 = el.is_utf16,
                    });
                    return .{ .e_string = item };
                },
                .e_inlined_enum => |el| {
                    const item = bun.create(allocator, E.InlinedEnum, .{
                        .value = el.value,
                        .comment = el.comment,
                    });
                    return .{ .e_inlined_enum = item };
                },
                else => this,
            };
        }

        /// `hasher` should be something with 'pub fn update([]const u8) void';
        /// symbol table is passed to serialize `Ref` as an identifier names instead of a nondeterministic numbers
        pub fn writeToHasher(this: Expr.Data, hasher: anytype, symbol_table: anytype) void {
            writeAnyToHasher(hasher, std.meta.activeTag(this));
            switch (this) {
                .e_name_of_symbol => |e| {
                    const symbol = e.ref.getSymbol(symbol_table);
                    hasher.update(symbol.original_name);
                },
                .e_array => |e| {
                    writeAnyToHasher(hasher, .{
                        e.is_single_line,
                        e.is_parenthesized,
                        e.was_originally_macro,
                        e.items.len,
                    });
                    for (e.items.slice()) |item| {
                        item.data.writeToHasher(hasher, symbol_table);
                    }
                },
                .e_unary => |e| {
                    writeAnyToHasher(hasher, .{e.op});
                    e.value.data.writeToHasher(hasher, symbol_table);
                },
                .e_binary => |e| {
                    writeAnyToHasher(hasher, .{e.op});
                    e.left.data.writeToHasher(hasher, symbol_table);
                    e.right.data.writeToHasher(hasher, symbol_table);
                },
                .e_class => |e| {
                    _ = e; // autofix
                },
                inline .e_new, .e_call => |e| {
                    _ = e; // autofix
                },
                .e_function => |e| {
                    _ = e; // autofix
                },
                .e_dot => |e| {
                    writeAnyToHasher(hasher, .{ e.optional_chain, e.name.len });
                    e.target.data.writeToHasher(hasher, symbol_table);
                    hasher.update(e.name);
                },
                .e_index => |e| {
                    writeAnyToHasher(hasher, .{e.optional_chain});
                    e.target.data.writeToHasher(hasher, symbol_table);
                    e.index.data.writeToHasher(hasher, symbol_table);
                },
                .e_arrow => |e| {
                    _ = e; // autofix
                },
                .e_jsx_element => |e| {
                    _ = e; // autofix
                },
                .e_object => |e| {
                    _ = e; // autofix
                },
                inline .e_spread, .e_await => |e| {
                    e.value.data.writeToHasher(hasher, symbol_table);
                },
                inline .e_yield => |e| {
                    writeAnyToHasher(hasher, .{ e.is_star, e.value });
                    if (e.value) |value|
                        value.data.writeToHasher(hasher, symbol_table);
                },
                .e_template => |e| {
                    _ = e; // autofix
                },
                .e_if => |e| {
                    _ = e; // autofix
                },
                .e_import => |e| {
                    _ = e; // autofix

                },
                inline .e_identifier,
                .e_import_identifier,
                .e_private_identifier,
                .e_commonjs_export_identifier,
                => |e| {
                    const symbol = e.ref.getSymbol(symbol_table);
                    hasher.update(symbol.original_name);
                },
                inline .e_boolean, .e_number => |e| {
                    writeAnyToHasher(hasher, e.value);
                },
                inline .e_big_int, .e_reg_exp => |e| {
                    hasher.update(e.value);
                },

                .e_string => |e| {
                    var next: ?*E.String = e;
                    if (next) |current| {
                        if (current.isUTF8()) {
                            hasher.update(current.data);
                        } else {
                            hasher.update(bun.reinterpretSlice(u8, current.slice16()));
                        }
                        next = current.next;
                        hasher.update("\x00");
                    }
                },
                inline .e_require_string, .e_require_resolve_string => |e| {
                    writeAnyToHasher(hasher, e.import_record_index); // preferably, i'd like to write the filepath
                },

                .e_import_meta_main => |e| {
                    writeAnyToHasher(hasher, e.inverted);
                },
                .e_inlined_enum => |e| {
                    // pretend there is no comment
                    e.value.data.writeToHasher(hasher, symbol_table);
                },

                // no data
                .e_require_call_target,
                .e_require_resolve_call_target,
                .e_missing,
                .e_this,
                .e_super,
                .e_null,
                .e_undefined,
                .e_new_target,
                .e_require_main,
                .e_import_meta,
                .e_special,
                => {},
            }
        }

        /// "const values" here refers to expressions that can participate in constant
        /// inlining, as they have no side effects on instantiation, and there would be
        /// no observable difference if duplicated. This is a subset of canBeMoved()
        pub fn canBeConstValue(this: Expr.Data) bool {
            return switch (this) {
                .e_number,
                .e_boolean,
                .e_null,
                .e_undefined,
                .e_inlined_enum,
                => true,
                .e_string => |str| str.next == null,
                .e_array => |array| array.was_originally_macro,
                .e_object => |object| object.was_originally_macro,
                else => false,
            };
        }

        /// Expressions that can be moved are those that do not have side
        /// effects on their own. This is used to determine what can be moved
        /// outside of a module wrapper (__esm/__commonJS).
        pub fn canBeMoved(data: Expr.Data) bool {
            return switch (data) {
                // TODO: identifiers can be removed if unused, however code that
                // moves expressions around sometimes does so incorrectly when
                // doing destructures. test case: https://github.com/oven-sh/bun/issues/14027
                // .e_identifier => |id| id.can_be_removed_if_unused,

                .e_class => |class| class.canBeMoved(),

                .e_arrow,
                .e_function,

                .e_number,
                .e_boolean,
                .e_null,
                .e_undefined,
                // .e_reg_exp,
                .e_big_int,
                .e_string,
                .e_inlined_enum,
                .e_import_meta,
                => true,

                .e_template => |template| template.tag == null and template.parts.len == 0,

                .e_array => |array| array.was_originally_macro,
                .e_object => |object| object.was_originally_macro,

                // TODO: experiment with allowing some e_binary, e_unary, e_if as movable

                else => false,
            };
        }

        pub fn knownPrimitive(data: Expr.Data) PrimitiveType {
            return switch (data) {
                .e_big_int => .bigint,
                .e_boolean => .boolean,
                .e_null => .null,
                .e_number => .number,
                .e_string => .string,
                .e_undefined => .undefined,
                .e_template => if (data.e_template.tag == null) PrimitiveType.string else PrimitiveType.unknown,
                .e_if => mergeKnownPrimitive(data.e_if.yes.data, data.e_if.no.data),
                .e_binary => |binary| brk: {
                    switch (binary.op) {
                        .bin_strict_eq,
                        .bin_strict_ne,
                        .bin_loose_eq,
                        .bin_loose_ne,
                        .bin_lt,
                        .bin_gt,
                        .bin_le,
                        .bin_ge,
                        .bin_instanceof,
                        .bin_in,
                        => break :brk PrimitiveType.boolean,
                        .bin_logical_or, .bin_logical_and => break :brk binary.left.data.mergeKnownPrimitive(binary.right.data),

                        .bin_nullish_coalescing => {
                            const left = binary.left.data.knownPrimitive();
                            const right = binary.right.data.knownPrimitive();
                            if (left == .null or left == .undefined)
                                break :brk right;

                            if (left != .unknown) {
                                if (left != .mixed)
                                    break :brk left; // Definitely not null or undefined

                                if (right != .unknown)
                                    break :brk PrimitiveType.mixed; // Definitely some kind of primitive
                            }
                        },

                        .bin_add => {
                            const left = binary.left.data.knownPrimitive();
                            const right = binary.right.data.knownPrimitive();

                            if (left == .string or right == .string)
                                break :brk PrimitiveType.string;

                            if (left == .bigint or right == .bigint)
                                break :brk PrimitiveType.bigint;

                            if (switch (left) {
                                .unknown, .mixed, .bigint => false,
                                else => true,
                            } and switch (right) {
                                .unknown, .mixed, .bigint => false,
                                else => true,
                            })
                                break :brk PrimitiveType.number;

                            break :brk PrimitiveType.mixed; // Can be number or bigint or string (or an exception)
                        },

                        .bin_sub,
                        .bin_sub_assign,
                        .bin_mul,
                        .bin_mul_assign,
                        .bin_div,
                        .bin_div_assign,
                        .bin_rem,
                        .bin_rem_assign,
                        .bin_pow,
                        .bin_pow_assign,
                        .bin_bitwise_and,
                        .bin_bitwise_and_assign,
                        .bin_bitwise_or,
                        .bin_bitwise_or_assign,
                        .bin_bitwise_xor,
                        .bin_bitwise_xor_assign,
                        .bin_shl,
                        .bin_shl_assign,
                        .bin_shr,
                        .bin_shr_assign,
                        .bin_u_shr,
                        .bin_u_shr_assign,
                        => break :brk PrimitiveType.mixed, // Can be number or bigint (or an exception)

                        .bin_assign,
                        .bin_comma,
                        => break :brk binary.right.data.knownPrimitive(),

                        else => {},
                    }

                    break :brk PrimitiveType.unknown;
                },

                .e_unary => switch (data.e_unary.op) {
                    .un_void => PrimitiveType.undefined,
                    .un_typeof => PrimitiveType.string,
                    .un_not, .un_delete => PrimitiveType.boolean,
                    .un_pos => PrimitiveType.number, // Cannot be bigint because that throws an exception
                    .un_neg, .un_cpl => switch (data.e_unary.value.data.knownPrimitive()) {
                        .bigint => PrimitiveType.bigint,
                        .unknown, .mixed => PrimitiveType.mixed,
                        else => PrimitiveType.number, // Can be number or bigint
                    },
                    .un_pre_dec, .un_pre_inc, .un_post_dec, .un_post_inc => PrimitiveType.mixed, // Can be number or bigint

                    else => PrimitiveType.unknown,
                },

                .e_inlined_enum => |inlined| inlined.value.data.knownPrimitive(),

                else => PrimitiveType.unknown,
            };
        }

        pub fn mergeKnownPrimitive(lhs: Expr.Data, rhs: Expr.Data) PrimitiveType {
            return lhs.knownPrimitive().merge(rhs.knownPrimitive());
        }

        /// Returns true if the result of the "typeof" operator on this expression is
        /// statically determined and this expression has no side effects (i.e. can be
        /// removed without consequence).
        pub inline fn toTypeof(data: Expr.Data) ?string {
            return @as(Expr.Tag, data).typeof();
        }

        pub fn toNumber(data: Expr.Data) ?f64 {
            return switch (data) {
                .e_null => 0,
                .e_undefined => std.math.nan(f64),
                .e_string => |str| {
                    if (str.next != null) return null;
                    if (!str.isUTF8()) return null;

                    // +'1' => 1
                    return stringToEquivalentNumberValue(str.slice8());
                },
                .e_boolean => @as(f64, if (data.e_boolean.value) 1.0 else 0.0),
                .e_number => data.e_number.value,
                .e_inlined_enum => |inlined| switch (inlined.value.data) {
                    .e_number => |num| num.value,
                    .e_string => |str| {
                        if (str.next != null) return null;
                        if (!str.isUTF8()) return null;

                        // +'1' => 1
                        return stringToEquivalentNumberValue(str.slice8());
                    },
                    else => null,
                },
                else => null,
            };
        }

        pub fn toFiniteNumber(data: Expr.Data) ?f64 {
            return switch (data) {
                .e_boolean => @as(f64, if (data.e_boolean.value) 1.0 else 0.0),
                .e_number => if (std.math.isFinite(data.e_number.value))
                    data.e_number.value
                else
                    null,
                .e_inlined_enum => |inlined| switch (inlined.value.data) {
                    .e_number => |num| if (std.math.isFinite(num.value))
                        num.value
                    else
                        null,
                    else => null,
                },
                else => null,
            };
        }

        pub fn extractNumericValue(data: Expr.Data) ?f64 {
            return switch (data) {
                .e_number => data.e_number.value,
                .e_inlined_enum => |inlined| switch (inlined.value.data) {
                    .e_number => |num| num.value,
                    else => null,
                },
                else => null,
            };
        }

        pub const Equality = struct {
            equal: bool = false,
            ok: bool = false,

            /// This extra flag is unfortunately required for the case of visiting the expression
            /// `require.main === module` (and any combination of !==, ==, !=, either ordering)
            ///
            /// We want to replace this with the dedicated import_meta_main node, which:
            /// - Stops this module from having p.require_ref, allowing conversion to ESM
            /// - Allows us to inline `import.meta.main`'s value, if it is known (bun build --compile)
            is_require_main_and_module: bool = false,

            pub const @"true" = Equality{ .ok = true, .equal = true };
            pub const @"false" = Equality{ .ok = true, .equal = false };
            pub const unknown = Equality{ .ok = false };
        };

        // Returns "equal, ok". If "ok" is false, then nothing is known about the two
        // values. If "ok" is true, the equality or inequality of the two values is
        // stored in "equal".
        pub fn eql(
            left: Expr.Data,
            right: Expr.Data,
            p: anytype,
            comptime kind: enum { loose, strict },
        ) Equality {
            comptime bun.assert(@typeInfo(@TypeOf(p)).pointer.size == .one); // pass *Parser

            // https://dorey.github.io/JavaScript-Equality-Table/
            switch (left) {
                .e_inlined_enum => |inlined| return inlined.value.data.eql(right, p, kind),

                .e_null, .e_undefined => {
                    const ok = switch (@as(Expr.Tag, right)) {
                        .e_null, .e_undefined => true,
                        else => @as(Expr.Tag, right).isPrimitiveLiteral(),
                    };

                    if (comptime kind == .loose) {
                        return .{
                            .equal = switch (@as(Expr.Tag, right)) {
                                .e_null, .e_undefined => true,
                                else => false,
                            },
                            .ok = ok,
                        };
                    }

                    return .{
                        .equal = @as(Tag, right) == @as(Tag, left),
                        .ok = ok,
                    };
                },
                .e_boolean => |l| {
                    switch (right) {
                        .e_boolean => {
                            return .{
                                .ok = true,
                                .equal = l.value == right.e_boolean.value,
                            };
                        },
                        .e_number => |num| {
                            if (comptime kind == .strict) {
                                // "true === 1" is false
                                // "false === 0" is false
                                return Equality.false;
                            }

                            return .{
                                .ok = true,
                                .equal = if (l.value)
                                    num.value == 1
                                else
                                    num.value == 0,
                            };
                        },
                        .e_null, .e_undefined => {
                            return Equality.false;
                        },
                        else => {},
                    }
                },
                .e_number => |l| {
                    switch (right) {
                        .e_number => |r| {
                            return .{
                                .ok = true,
                                .equal = l.value == r.value,
                            };
                        },
                        .e_inlined_enum => |r| if (r.value.data == .e_number) {
                            return .{
                                .ok = true,
                                .equal = l.value == r.value.data.e_number.value,
                            };
                        },
                        .e_boolean => |r| {
                            if (comptime kind == .loose) {
                                return .{
                                    .ok = true,
                                    // "1 == true" is true
                                    // "0 == false" is true
                                    .equal = if (r.value)
                                        l.value == 1
                                    else
                                        l.value == 0,
                                };
                            }

                            // "1 === true" is false
                            // "0 === false" is false
                            return Equality.false;
                        },
                        .e_null, .e_undefined => {
                            // "(not null or undefined) == undefined" is false
                            return Equality.false;
                        },
                        else => {},
                    }
                },
                .e_big_int => |l| {
                    if (right == .e_big_int) {
                        if (strings.eqlLong(l.value, right.e_big_int.value, true)) {
                            return Equality.true;
                        }

                        // 0x0000n == 0n is true
                        return .{ .ok = false };
                    } else {
                        return .{
                            .ok = switch (right) {
                                .e_null, .e_undefined => true,
                                else => false,
                            },
                            .equal = false,
                        };
                    }
                },
                .e_string => |l| {
                    switch (right) {
                        .e_string => |r| {
                            r.resolveRopeIfNeeded(p.allocator);
                            l.resolveRopeIfNeeded(p.allocator);
                            return .{
                                .ok = true,
                                .equal = r.eql(E.String, l),
                            };
                        },
                        .e_inlined_enum => |inlined| {
                            if (inlined.value.data == .e_string) {
                                const r = inlined.value.data.e_string;

                                r.resolveRopeIfNeeded(p.allocator);
                                l.resolveRopeIfNeeded(p.allocator);

                                return .{
                                    .ok = true,
                                    .equal = r.eql(E.String, l),
                                };
                            }
                        },
                        .e_null, .e_undefined => {
                            return Equality.false;
                        },
                        .e_number => |r| {
                            if (comptime kind == .loose) {
                                l.resolveRopeIfNeeded(p.allocator);
                                if (r.value == 0 and (l.isBlank() or l.eqlComptime("0"))) {
                                    return Equality.true;
                                }

                                if (r.value == 1 and l.eqlComptime("1")) {
                                    return Equality.true;
                                }

                                // the string could still equal 0 or 1 but it could be hex, binary, octal, ...
                                return Equality.unknown;
                            } else {
                                return Equality.false;
                            }
                        },

                        else => {},
                    }
                },

                else => {
                    // Do not need to check left because e_require_main is
                    // always re-ordered to the right side.
                    if (right == .e_require_main) {
                        if (left.as(.e_identifier)) |id| {
                            if (id.ref.eql(p.module_ref)) return .{
                                .ok = true,
                                .equal = true,
                                .is_require_main_and_module = true,
                            };
                        }
                    }
                },
            }

            return Equality.unknown;
        }

        pub fn toJS(this: Data, allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) ToJSError!JSC.JSValue {
            return switch (this) {
                .e_array => |e| e.toJS(allocator, globalObject),
                .e_object => |e| e.toJS(allocator, globalObject),
                .e_string => |e| e.toJS(allocator, globalObject),
                .e_null => JSC.JSValue.null,
                .e_undefined => JSC.JSValue.undefined,
                .e_boolean => |boolean| if (boolean.value)
                    JSC.JSValue.true
                else
                    JSC.JSValue.false,
                .e_number => |e| e.toJS(),
                // .e_big_int => |e| e.toJS(ctx, exception),

                .e_inlined_enum => |inlined| inlined.value.data.toJS(allocator, globalObject),

                .e_identifier,
                .e_import_identifier,
                .e_private_identifier,
                .e_commonjs_export_identifier,
                => error.@"Cannot convert identifier to JS. Try a statically-known value",

                // brk: {
                //     // var node = try allocator.create(Macro.JSNode);
                //     // node.* = Macro.JSNode.initExpr(Expr{ .data = this, .loc = logger.Loc.Empty });
                //     // break :brk JSC.JSValue.c(Macro.JSNode.Class.make(globalObject, node));
                // },

                else => {
                    return error.@"Cannot convert argument type to JS";
                },
            };
        }

        pub const Store = struct {
            const StoreType = NewStore(&.{
                E.NameOfSymbol,
                E.Array,
                E.Arrow,
                E.Await,
                E.BigInt,
                E.Binary,
                E.Call,
                E.Class,
                E.Dot,
                E.Function,
                E.If,
                E.Import,
                E.Index,
                E.InlinedEnum,
                E.JSXElement,
                E.New,
                E.Number,
                E.Object,
                E.PrivateIdentifier,
                E.RegExp,
                E.Spread,
                E.String,
                E.Template,
                E.TemplatePart,
                E.Unary,
                E.Yield,
            }, 512);

            pub threadlocal var instance: ?*StoreType = null;
            pub threadlocal var memory_allocator: ?*ASTMemoryAllocator = null;
            pub threadlocal var disable_reset = false;

            pub fn create() void {
                if (instance != null or memory_allocator != null) {
                    return;
                }

                instance = StoreType.init();
            }

            pub fn reset() void {
                if (disable_reset or memory_allocator != null) return;
                instance.?.reset();
            }

            pub fn deinit() void {
                if (instance == null or memory_allocator != null) return;
                instance.?.deinit();
                instance = null;
            }

            pub inline fn assert() void {
                if (comptime Environment.isDebug or Environment.enable_asan) {
                    if (instance == null and memory_allocator == null)
                        bun.unreachablePanic("Store must be init'd", .{});
                }
            }

            /// create || reset
            pub fn begin() void {
                if (memory_allocator != null) return;
                if (instance == null) {
                    create();
                    return;
                }

                if (!disable_reset)
                    instance.?.reset();
            }

            pub fn append(comptime T: type, value: T) *T {
                if (memory_allocator) |allocator| {
                    return allocator.append(T, value);
                }

                Disabler.assert();
                return instance.?.append(T, value);
            }
        };

        pub inline fn isStringValue(self: Data) bool {
            return @as(Expr.Tag, self) == .e_string;
        }
    };

    pub fn StoredData(tag: Tag) type {
        const T = @FieldType(Data, tag);
        return switch (@typeInfo(T)) {
            .pointer => |ptr| ptr.child,
            else => T,
        };
    }
};

pub const EnumValue = struct {
    loc: logger.Loc,
    ref: Ref,
    name: []const u8,
    value: ?ExprNodeIndex,

    pub fn nameAsEString(enum_value: EnumValue, allocator: std.mem.Allocator) E.String {
        return E.String.initReEncodeUTF8(enum_value.name, allocator);
    }
};

pub const S = struct {
    pub const Block = struct {
        stmts: StmtNodeList,
        close_brace_loc: logger.Loc = logger.Loc.Empty,
    };

    pub const SExpr = struct {
        value: ExprNodeIndex,

        // This is set to true for automatically-generated expressions that should
        // not affect tree shaking. For example, calling a function from the runtime
        // that doesn't have externally-visible side effects.
        does_not_affect_tree_shaking: bool = false,
    };

    pub const Comment = struct { text: string };

    pub const Directive = struct {
        value: []const u8,
    };

    pub const ExportClause = struct {
        items: []ClauseItem,
        is_single_line: bool,
    };

    pub const Empty = struct {};

    pub const ExportStar = struct {
        namespace_ref: Ref,
        alias: ?G.ExportStarAlias = null,
        import_record_index: u32,
    };

    // This is an "export = value;" statement in TypeScript
    pub const ExportEquals = struct { value: ExprNodeIndex };

    pub const Label = struct { name: LocRef, stmt: StmtNodeIndex };

    // This is a stand-in for a TypeScript type declaration
    pub const TypeScript = struct {};

    pub const Debugger = struct {};

    pub const ExportFrom = struct {
        items: []ClauseItem,
        namespace_ref: Ref,
        import_record_index: u32,
        is_single_line: bool,
    };

    pub const ExportDefault = struct {
        default_name: LocRef, // value may be a SFunction or SClass
        value: StmtOrExpr,

        pub fn canBeMoved(self: *const ExportDefault) bool {
            return switch (self.value) {
                .expr => |e| e.canBeMoved(),
                .stmt => |s| switch (s.data) {
                    .s_class => |class| class.class.canBeMoved(),
                    .s_function => true,
                    else => false,
                },
            };
        }
    };

    pub const Enum = struct {
        name: LocRef,
        arg: Ref,
        values: []EnumValue,
        is_export: bool,
    };

    pub const Namespace = struct {
        name: LocRef,
        arg: Ref,
        stmts: StmtNodeList,
        is_export: bool,
    };

    pub const Function = struct {
        func: G.Fn,
    };

    pub const Class = struct { class: G.Class, is_export: bool = false };

    pub const If = struct {
        test_: ExprNodeIndex,
        yes: StmtNodeIndex,
        no: ?StmtNodeIndex,
    };

    pub const For = struct {
        // May be a SConst, SLet, SVar, or SExpr
        init: ?StmtNodeIndex = null,
        test_: ?ExprNodeIndex = null,
        update: ?ExprNodeIndex = null,
        body: StmtNodeIndex,
    };

    pub const ForIn = struct {
        // May be a SConst, SLet, SVar, or SExpr
        init: StmtNodeIndex,
        value: ExprNodeIndex,
        body: StmtNodeIndex,
    };

    pub const ForOf = struct {
        is_await: bool = false,
        // May be a SConst, SLet, SVar, or SExpr
        init: StmtNodeIndex,
        value: ExprNodeIndex,
        body: StmtNodeIndex,
    };

    pub const DoWhile = struct { body: StmtNodeIndex, test_: ExprNodeIndex };

    pub const While = struct {
        test_: ExprNodeIndex,
        body: StmtNodeIndex,
    };

    pub const With = struct {
        value: ExprNodeIndex,
        body: StmtNodeIndex,
        body_loc: logger.Loc = logger.Loc.Empty,
    };

    pub const Try = struct {
        body_loc: logger.Loc,
        body: StmtNodeList,

        catch_: ?Catch = null,
        finally: ?Finally = null,
    };

    pub const Switch = struct {
        test_: ExprNodeIndex,
        body_loc: logger.Loc,
        cases: []Case,
    };

    // This object represents all of these types of import statements:
    //
    //    import 'path'
    //    import {item1, item2} from 'path'
    //    import * as ns from 'path'
    //    import defaultItem, {item1, item2} from 'path'
    //    import defaultItem, * as ns from 'path'
    //
    // Many parts are optional and can be combined in different ways. The only
    // restriction is that you cannot have both a clause and a star namespace.
    pub const Import = struct {
        // If this is a star import: This is a Ref for the namespace symbol. The Loc
        // for the symbol is StarLoc.
        //
        // Otherwise: This is an auto-generated Ref for the namespace representing
        // the imported file. In this case StarLoc is nil. The NamespaceRef is used
        // when converting this module to a CommonJS module.
        namespace_ref: Ref,
        default_name: ?LocRef = null,
        items: []ClauseItem = &.{},
        star_name_loc: ?logger.Loc = null,
        import_record_index: u32,
        is_single_line: bool = false,
    };

    pub const Return = struct { value: ?ExprNodeIndex = null };
    pub const Throw = struct { value: ExprNodeIndex };

    pub const Local = struct {
        kind: Kind = .k_var,
        decls: G.Decl.List = .{},
        is_export: bool = false,
        // The TypeScript compiler doesn't generate code for "import foo = bar"
        // statements where the import is never used.
        was_ts_import_equals: bool = false,

        was_commonjs_export: bool = false,

        pub fn canMergeWith(this: *const Local, other: *const Local) bool {
            return this.kind == other.kind and this.is_export == other.is_export and
                this.was_commonjs_export == other.was_commonjs_export;
        }

        pub const Kind = enum {
            k_var,
            k_let,
            k_const,
            k_using,
            k_await_using,

            pub fn jsonStringify(self: @This(), writer: anytype) !void {
                return try writer.write(@tagName(self));
            }

            pub fn isUsing(self: Kind) bool {
                return self == .k_using or self == .k_await_using;
            }

            pub fn isReassignable(kind: Kind) bool {
                return kind == .k_var or kind == .k_let;
            }
        };
    };

    pub const Break = struct {
        label: ?LocRef = null,
    };

    pub const Continue = struct {
        label: ?LocRef = null,
    };
};

pub const Catch = struct {
    loc: logger.Loc,
    binding: ?BindingNodeIndex = null,
    body: StmtNodeList,
    body_loc: logger.Loc,
};

pub const Finally = struct {
    loc: logger.Loc,
    stmts: StmtNodeList,
};

pub const Case = struct { loc: logger.Loc, value: ?ExprNodeIndex, body: StmtNodeList };

pub const Op = struct {
    // If you add a new token, remember to add it to "Table" too
    pub const Code = enum {
        // Prefix
        un_pos, // +expr
        un_neg, // -expr
        un_cpl, // ~expr
        un_not, // !expr
        un_void,
        un_typeof,
        un_delete,

        // Prefix update
        un_pre_dec,
        un_pre_inc,

        // Postfix update
        un_post_dec,
        un_post_inc,

        /// Left-associative
        bin_add,
        /// Left-associative
        bin_sub,
        /// Left-associative
        bin_mul,
        /// Left-associative
        bin_div,
        /// Left-associative
        bin_rem,
        /// Left-associative
        bin_pow,
        /// Left-associative
        bin_lt,
        /// Left-associative
        bin_le,
        /// Left-associative
        bin_gt,
        /// Left-associative
        bin_ge,
        /// Left-associative
        bin_in,
        /// Left-associative
        bin_instanceof,
        /// Left-associative
        bin_shl,
        /// Left-associative
        bin_shr,
        /// Left-associative
        bin_u_shr,
        /// Left-associative
        bin_loose_eq,
        /// Left-associative
        bin_loose_ne,
        /// Left-associative
        bin_strict_eq,
        /// Left-associative
        bin_strict_ne,
        /// Left-associative
        bin_nullish_coalescing,
        /// Left-associative
        bin_logical_or,
        /// Left-associative
        bin_logical_and,
        /// Left-associative
        bin_bitwise_or,
        /// Left-associative
        bin_bitwise_and,
        /// Left-associative
        bin_bitwise_xor,

        /// Non-associative
        bin_comma,

        /// Right-associative
        bin_assign,
        /// Right-associative
        bin_add_assign,
        /// Right-associative
        bin_sub_assign,
        /// Right-associative
        bin_mul_assign,
        /// Right-associative
        bin_div_assign,
        /// Right-associative
        bin_rem_assign,
        /// Right-associative
        bin_pow_assign,
        /// Right-associative
        bin_shl_assign,
        /// Right-associative
        bin_shr_assign,
        /// Right-associative
        bin_u_shr_assign,
        /// Right-associative
        bin_bitwise_or_assign,
        /// Right-associative
        bin_bitwise_and_assign,
        /// Right-associative
        bin_bitwise_xor_assign,
        /// Right-associative
        bin_nullish_coalescing_assign,
        /// Right-associative
        bin_logical_or_assign,
        /// Right-associative
        bin_logical_and_assign,

        pub fn jsonStringify(self: @This(), writer: anytype) !void {
            return try writer.write(@tagName(self));
        }

        pub fn unaryAssignTarget(code: Op.Code) AssignTarget {
            if (@intFromEnum(code) >=
                @intFromEnum(Op.Code.un_pre_dec) and @intFromEnum(code) <=
                @intFromEnum(Op.Code.un_post_inc))
            {
                return AssignTarget.update;
            }

            return AssignTarget.none;
        }
        pub fn isLeftAssociative(code: Op.Code) bool {
            return @intFromEnum(code) >=
                @intFromEnum(Op.Code.bin_add) and
                @intFromEnum(code) < @intFromEnum(Op.Code.bin_comma) and code != .bin_pow;
        }
        pub fn isRightAssociative(code: Op.Code) bool {
            return @intFromEnum(code) >= @intFromEnum(Op.Code.bin_assign) or code == .bin_pow;
        }
        pub fn binaryAssignTarget(code: Op.Code) AssignTarget {
            if (code == .bin_assign) {
                return AssignTarget.replace;
            }

            if (@intFromEnum(code) > @intFromEnum(Op.Code.bin_assign)) {
                return AssignTarget.update;
            }

            return AssignTarget.none;
        }

        pub fn isPrefix(code: Op.Code) bool {
            return @intFromEnum(code) < @intFromEnum(Op.Code.un_post_dec);
        }
    };

    pub const Level = enum(u6) {
        lowest,
        comma,
        spread,
        yield,
        assign,
        conditional,
        nullish_coalescing,
        logical_or,
        logical_and,
        bitwise_or,
        bitwise_xor,
        bitwise_and,
        equals,
        compare,
        shift,
        add,
        multiply,
        exponentiation,
        prefix,
        postfix,
        new,
        call,
        member,

        pub inline fn lt(self: Level, b: Level) bool {
            return @intFromEnum(self) < @intFromEnum(b);
        }
        pub inline fn gt(self: Level, b: Level) bool {
            return @intFromEnum(self) > @intFromEnum(b);
        }
        pub inline fn gte(self: Level, b: Level) bool {
            return @intFromEnum(self) >= @intFromEnum(b);
        }
        pub inline fn lte(self: Level, b: Level) bool {
            return @intFromEnum(self) <= @intFromEnum(b);
        }
        pub inline fn eql(self: Level, b: Level) bool {
            return @intFromEnum(self) == @intFromEnum(b);
        }

        pub inline fn sub(self: Level, i: anytype) Level {
            return @as(Level, @enumFromInt(@intFromEnum(self) - i));
        }

        pub inline fn addF(self: Level, i: anytype) Level {
            return @as(Level, @enumFromInt(@intFromEnum(self) + i));
        }
    };

    text: string,
    level: Level,
    is_keyword: bool = false,

    pub fn init(triple: anytype) Op {
        return Op{
            .text = triple.@"0",
            .level = triple.@"1",
            .is_keyword = triple.@"2",
        };
    }

    pub fn jsonStringify(self: *const @This(), writer: anytype) !void {
        return try writer.write(self.text);
    }

    pub const TableType: std.EnumArray(Op.Code, Op) = undefined;
    pub const Table = brk: {
        var table = std.EnumArray(Op.Code, Op).initUndefined();

        // Prefix
        table.set(Op.Code.un_pos, Op.init(.{ "+", Level.prefix, false }));
        table.set(Op.Code.un_neg, Op.init(.{ "-", Level.prefix, false }));
        table.set(Op.Code.un_cpl, Op.init(.{ "~", Level.prefix, false }));
        table.set(Op.Code.un_not, Op.init(.{ "!", Level.prefix, false }));
        table.set(Op.Code.un_void, Op.init(.{ "void", Level.prefix, true }));
        table.set(Op.Code.un_typeof, Op.init(.{ "typeof", Level.prefix, true }));
        table.set(Op.Code.un_delete, Op.init(.{ "delete", Level.prefix, true }));

        // Prefix update
        table.set(Op.Code.un_pre_dec, Op.init(.{ "--", Level.prefix, false }));
        table.set(Op.Code.un_pre_inc, Op.init(.{ "++", Level.prefix, false }));

        // Postfix update
        table.set(Op.Code.un_post_dec, Op.init(.{ "--", Level.postfix, false }));
        table.set(Op.Code.un_post_inc, Op.init(.{ "++", Level.postfix, false }));

        // Left-associative
        table.set(Op.Code.bin_add, Op.init(.{ "+", Level.add, false }));
        table.set(Op.Code.bin_sub, Op.init(.{ "-", Level.add, false }));
        table.set(Op.Code.bin_mul, Op.init(.{ "*", Level.multiply, false }));
        table.set(Op.Code.bin_div, Op.init(.{ "/", Level.multiply, false }));
        table.set(Op.Code.bin_rem, Op.init(.{ "%", Level.multiply, false }));
        table.set(Op.Code.bin_pow, Op.init(.{ "**", Level.exponentiation, false }));
        table.set(Op.Code.bin_lt, Op.init(.{ "<", Level.compare, false }));
        table.set(Op.Code.bin_le, Op.init(.{ "<=", Level.compare, false }));
        table.set(Op.Code.bin_gt, Op.init(.{ ">", Level.compare, false }));
        table.set(Op.Code.bin_ge, Op.init(.{ ">=", Level.compare, false }));
        table.set(Op.Code.bin_in, Op.init(.{ "in", Level.compare, true }));
        table.set(Op.Code.bin_instanceof, Op.init(.{ "instanceof", Level.compare, true }));
        table.set(Op.Code.bin_shl, Op.init(.{ "<<", Level.shift, false }));
        table.set(Op.Code.bin_shr, Op.init(.{ ">>", Level.shift, false }));
        table.set(Op.Code.bin_u_shr, Op.init(.{ ">>>", Level.shift, false }));
        table.set(Op.Code.bin_loose_eq, Op.init(.{ "==", Level.equals, false }));
        table.set(Op.Code.bin_loose_ne, Op.init(.{ "!=", Level.equals, false }));
        table.set(Op.Code.bin_strict_eq, Op.init(.{ "===", Level.equals, false }));
        table.set(Op.Code.bin_strict_ne, Op.init(.{ "!==", Level.equals, false }));
        table.set(Op.Code.bin_nullish_coalescing, Op.init(.{ "??", Level.nullish_coalescing, false }));
        table.set(Op.Code.bin_logical_or, Op.init(.{ "||", Level.logical_or, false }));
        table.set(Op.Code.bin_logical_and, Op.init(.{ "&&", Level.logical_and, false }));
        table.set(Op.Code.bin_bitwise_or, Op.init(.{ "|", Level.bitwise_or, false }));
        table.set(Op.Code.bin_bitwise_and, Op.init(.{ "&", Level.bitwise_and, false }));
        table.set(Op.Code.bin_bitwise_xor, Op.init(.{ "^", Level.bitwise_xor, false }));

        // Non-associative
        table.set(Op.Code.bin_comma, Op.init(.{ ",", Level.comma, false }));

        // Right-associative
        table.set(Op.Code.bin_assign, Op.init(.{ "=", Level.assign, false }));
        table.set(Op.Code.bin_add_assign, Op.init(.{ "+=", Level.assign, false }));
        table.set(Op.Code.bin_sub_assign, Op.init(.{ "-=", Level.assign, false }));
        table.set(Op.Code.bin_mul_assign, Op.init(.{ "*=", Level.assign, false }));
        table.set(Op.Code.bin_div_assign, Op.init(.{ "/=", Level.assign, false }));
        table.set(Op.Code.bin_rem_assign, Op.init(.{ "%=", Level.assign, false }));
        table.set(Op.Code.bin_pow_assign, Op.init(.{ "**=", Level.assign, false }));
        table.set(Op.Code.bin_shl_assign, Op.init(.{ "<<=", Level.assign, false }));
        table.set(Op.Code.bin_shr_assign, Op.init(.{ ">>=", Level.assign, false }));
        table.set(Op.Code.bin_u_shr_assign, Op.init(.{ ">>>=", Level.assign, false }));
        table.set(Op.Code.bin_bitwise_or_assign, Op.init(.{ "|=", Level.assign, false }));
        table.set(Op.Code.bin_bitwise_and_assign, Op.init(.{ "&=", Level.assign, false }));
        table.set(Op.Code.bin_bitwise_xor_assign, Op.init(.{ "^=", Level.assign, false }));
        table.set(Op.Code.bin_nullish_coalescing_assign, Op.init(.{ "??=", Level.assign, false }));
        table.set(Op.Code.bin_logical_or_assign, Op.init(.{ "||=", Level.assign, false }));
        table.set(Op.Code.bin_logical_and_assign, Op.init(.{ "&&=", Level.assign, false }));

        break :brk table;
    };
};

pub const ArrayBinding = struct {
    binding: BindingNodeIndex,
    default_value: ?ExprNodeIndex = null,
};

pub const Ast = struct {
    pub const TopLevelSymbolToParts = std.ArrayHashMapUnmanaged(Ref, BabyList(u32), Ref.ArrayHashCtx, false);

    approximate_newline_count: usize = 0,
    has_lazy_export: bool = false,
    runtime_imports: Runtime.Imports = .{},

    nested_scope_slot_counts: SlotCounts = SlotCounts{},

    runtime_import_record_id: ?u32 = null,
    needs_runtime: bool = false,
    // This is a list of CommonJS features. When a file uses CommonJS features,
    // it's not a candidate for "flat bundling" and must be wrapped in its own
    // closure.
    has_top_level_return: bool = false,
    uses_exports_ref: bool = false,
    uses_module_ref: bool = false,
    uses_require_ref: bool = false,
    commonjs_module_exports_assigned_deoptimized: bool = false,

    force_cjs_to_esm: bool = false,
    exports_kind: ExportsKind = ExportsKind.none,

    // This is a list of ES6 features. They are ranges instead of booleans so
    // that they can be used in log messages. Check to see if "Len > 0".
    import_keyword: logger.Range = logger.Range.None, // Does not include TypeScript-specific syntax or "import()"
    export_keyword: logger.Range = logger.Range.None, // Does not include TypeScript-specific syntax
    top_level_await_keyword: logger.Range = logger.Range.None,

    /// These are stored at the AST level instead of on individual AST nodes so
    /// they can be manipulated efficiently without a full AST traversal
    import_records: ImportRecord.List = .{},

    hashbang: string = "",
    directive: ?string = null,
    parts: Part.List = Part.List{},
    // This list may be mutated later, so we should store the capacity
    symbols: Symbol.List = Symbol.List{},
    module_scope: Scope = Scope{},
    char_freq: ?CharFreq = null,
    exports_ref: Ref = Ref.None,
    module_ref: Ref = Ref.None,
    /// When using format .bake_internal_dev, this is the HMR variable instead
    /// of the wrapper. This is because that format does not store module
    /// wrappers in a variable.
    wrapper_ref: Ref = Ref.None,
    require_ref: Ref = Ref.None,

    // These are used when bundling. They are filled in during the parser pass
    // since we already have to traverse the AST then anyway and the parser pass
    // is conveniently fully parallelized.
    named_imports: NamedImports = .{},
    named_exports: NamedExports = .{},
    export_star_import_records: []u32 = &([_]u32{}),

    // allocator: std.mem.Allocator,
    top_level_symbols_to_parts: TopLevelSymbolToParts = .{},

    commonjs_named_exports: CommonJSNamedExports = .{},

    redirect_import_record_index: ?u32 = null,

    /// Only populated when bundling
    target: bun.options.Target = .browser,
    // const_values: ConstValuesMap = .{},
    ts_enums: TsEnumsMap = .{},

    /// Not to be confused with `commonjs_named_exports`
    /// This is a list of named exports that may exist in a CommonJS module
    /// We use this with `commonjs_at_runtime` to re-export CommonJS
    has_commonjs_export_names: bool = false,
    import_meta_ref: Ref = Ref.None,

    pub const CommonJSNamedExport = struct {
        loc_ref: LocRef,
        needs_decl: bool = true,
    };
    pub const CommonJSNamedExports = bun.StringArrayHashMapUnmanaged(CommonJSNamedExport);

    pub const NamedImports = std.ArrayHashMapUnmanaged(Ref, NamedImport, RefHashCtx, true);
    pub const NamedExports = bun.StringArrayHashMapUnmanaged(NamedExport);
    pub const ConstValuesMap = std.ArrayHashMapUnmanaged(Ref, Expr, RefHashCtx, false);
    pub const TsEnumsMap = std.ArrayHashMapUnmanaged(Ref, bun.StringHashMapUnmanaged(InlinedEnumValue), RefHashCtx, false);

    pub fn fromParts(parts: []Part) Ast {
        return Ast{
            .parts = Part.List.init(parts),
            .runtime_imports = .{},
        };
    }

    pub fn initTest(parts: []Part) Ast {
        return Ast{
            .parts = Part.List.init(parts),
            .runtime_imports = .{},
        };
    }

    pub const empty = Ast{ .parts = Part.List{}, .runtime_imports = .{} };

    pub fn toJSON(self: *const Ast, _: std.mem.Allocator, stream: anytype) !void {
        const opts = std.json.StringifyOptions{ .whitespace = std.json.StringifyOptions.Whitespace{
            .separator = true,
        } };
        try std.json.stringify(self.parts, opts, stream);
    }

    /// Do not call this if it wasn't globally allocated!
    pub fn deinit(this: *Ast) void {
        // TODO: assert mimalloc-owned memory
        if (this.parts.len > 0) this.parts.deinitWithAllocator(bun.default_allocator);
        if (this.symbols.len > 0) this.symbols.deinitWithAllocator(bun.default_allocator);
        if (this.import_records.len > 0) this.import_records.deinitWithAllocator(bun.default_allocator);
    }
};

/// TLA => Top Level Await
pub const TlaCheck = struct {
    depth: u32 = 0,
    parent: Index.Int = Index.invalid.get(),
    import_record_index: Index.Int = Index.invalid.get(),
};

/// Like Ast but slimmer and for bundling only.
///
/// On Linux, the hottest function in the bundler is:
/// src.multi_array_list.MultiArrayList(src.js_ast.Ast).ensureTotalCapacity
/// https://share.firefox.dev/3NNlRKt
///
/// So we make a slimmer version of Ast for bundling that doesn't allocate as much memory
pub const BundledAst = struct {
    approximate_newline_count: u32 = 0,
    nested_scope_slot_counts: SlotCounts = .{},

    exports_kind: ExportsKind = .none,

    /// These are stored at the AST level instead of on individual AST nodes so
    /// they can be manipulated efficiently without a full AST traversal
    import_records: ImportRecord.List = .{},

    hashbang: string = "",
    parts: Part.List = .{},
    css: ?*bun.css.BundlerStyleSheet = null,
    url_for_css: []const u8 = "",
    symbols: Symbol.List = .{},
    module_scope: Scope = .{},
    char_freq: CharFreq = undefined,
    exports_ref: Ref = Ref.None,
    module_ref: Ref = Ref.None,
    wrapper_ref: Ref = Ref.None,
    require_ref: Ref = Ref.None,
    top_level_await_keyword: logger.Range,
    tla_check: TlaCheck = .{},

    // These are used when bundling. They are filled in during the parser pass
    // since we already have to traverse the AST then anyway and the parser pass
    // is conveniently fully parallelized.
    named_imports: NamedImports = .{},
    named_exports: NamedExports = .{},
    export_star_import_records: []u32 = &.{},

    top_level_symbols_to_parts: TopLevelSymbolToParts = .{},

    commonjs_named_exports: CommonJSNamedExports = .{},

    redirect_import_record_index: u32 = std.math.maxInt(u32),

    /// Only populated when bundling. When --server-components is passed, this
    /// will be .browser when it is a client component, and the server's target
    /// on the server.
    target: bun.options.Target = .browser,

    // const_values: ConstValuesMap = .{},
    ts_enums: Ast.TsEnumsMap = .{},

    flags: BundledAst.Flags = .{},

    pub const NamedImports = Ast.NamedImports;
    pub const NamedExports = Ast.NamedExports;
    pub const TopLevelSymbolToParts = Ast.TopLevelSymbolToParts;
    pub const CommonJSNamedExports = Ast.CommonJSNamedExports;
    pub const ConstValuesMap = Ast.ConstValuesMap;

    pub const Flags = packed struct(u8) {
        // This is a list of CommonJS features. When a file uses CommonJS features,
        // it's not a candidate for "flat bundling" and must be wrapped in its own
        // closure.
        uses_exports_ref: bool = false,
        uses_module_ref: bool = false,
        // uses_require_ref: bool = false,
        uses_export_keyword: bool = false,
        has_char_freq: bool = false,
        force_cjs_to_esm: bool = false,
        has_lazy_export: bool = false,
        commonjs_module_exports_assigned_deoptimized: bool = false,
        has_explicit_use_strict_directive: bool = false,
    };

    pub const empty = BundledAst.init(Ast.empty);

    pub fn toAST(this: *const BundledAst) Ast {
        return .{
            .approximate_newline_count = this.approximate_newline_count,
            .nested_scope_slot_counts = this.nested_scope_slot_counts,

            .exports_kind = this.exports_kind,

            .import_records = this.import_records,

            .hashbang = this.hashbang,
            .parts = this.parts,
            // This list may be mutated later, so we should store the capacity
            .symbols = this.symbols,
            .module_scope = this.module_scope,
            .char_freq = if (this.flags.has_char_freq) this.char_freq else null,
            .exports_ref = this.exports_ref,
            .module_ref = this.module_ref,
            .wrapper_ref = this.wrapper_ref,
            .require_ref = this.require_ref,
            .top_level_await_keyword = this.top_level_await_keyword,

            // These are used when bundling. They are filled in during the parser pass
            // since we already have to traverse the AST then anyway and the parser pass
            // is conveniently fully parallelized.
            .named_imports = this.named_imports,
            .named_exports = this.named_exports,
            .export_star_import_records = this.export_star_import_records,

            .top_level_symbols_to_parts = this.top_level_symbols_to_parts,

            .commonjs_named_exports = this.commonjs_named_exports,

            .redirect_import_record_index = this.redirect_import_record_index,

            .target = this.target,

            // .const_values = this.const_values,
            .ts_enums = this.ts_enums,

            .uses_exports_ref = this.flags.uses_exports_ref,
            .uses_module_ref = this.flags.uses_module_ref,
            // .uses_require_ref = ast.uses_require_ref,
            .export_keyword = .{ .len = if (this.flags.uses_export_keyword) 1 else 0, .loc = .{} },
            .force_cjs_to_esm = this.flags.force_cjs_to_esm,
            .has_lazy_export = this.flags.has_lazy_export,
            .commonjs_module_exports_assigned_deoptimized = this.flags.commonjs_module_exports_assigned_deoptimized,
            .directive = if (this.flags.has_explicit_use_strict_directive) "use strict" else null,
        };
    }

    pub fn init(ast: Ast) BundledAst {
        return .{
            .approximate_newline_count = @as(u32, @truncate(ast.approximate_newline_count)),
            .nested_scope_slot_counts = ast.nested_scope_slot_counts,

            .exports_kind = ast.exports_kind,

            .import_records = ast.import_records,

            .hashbang = ast.hashbang,
            .parts = ast.parts,
            // This list may be mutated later, so we should store the capacity
            .symbols = ast.symbols,
            .module_scope = ast.module_scope,
            .char_freq = ast.char_freq orelse undefined,
            .exports_ref = ast.exports_ref,
            .module_ref = ast.module_ref,
            .wrapper_ref = ast.wrapper_ref,
            .require_ref = ast.require_ref,
            .top_level_await_keyword = ast.top_level_await_keyword,
            // These are used when bundling. They are filled in during the parser pass
            // since we already have to traverse the AST then anyway and the parser pass
            // is conveniently fully parallelized.
            .named_imports = ast.named_imports,
            .named_exports = ast.named_exports,
            .export_star_import_records = ast.export_star_import_records,

            // .allocator = ast.allocator,
            .top_level_symbols_to_parts = ast.top_level_symbols_to_parts,

            .commonjs_named_exports = ast.commonjs_named_exports,

            .redirect_import_record_index = ast.redirect_import_record_index orelse std.math.maxInt(u32),

            .target = ast.target,

            // .const_values = ast.const_values,
            .ts_enums = ast.ts_enums,

            .flags = .{
                .uses_exports_ref = ast.uses_exports_ref,
                .uses_module_ref = ast.uses_module_ref,
                // .uses_require_ref = ast.uses_require_ref,
                .uses_export_keyword = ast.export_keyword.len > 0,
                .has_char_freq = ast.char_freq != null,
                .force_cjs_to_esm = ast.force_cjs_to_esm,
                .has_lazy_export = ast.has_lazy_export,
                .commonjs_module_exports_assigned_deoptimized = ast.commonjs_module_exports_assigned_deoptimized,
                .has_explicit_use_strict_directive = strings.eqlComptime(ast.directive orelse "", "use strict"),
            },
        };
    }

    /// TODO: Move this from being done on all parse tasks into the start of the linker. This currently allocates base64 encoding for every small file loaded thing.
    pub fn addUrlForCss(
        this: *BundledAst,
        allocator: std.mem.Allocator,
        source: *const logger.Source,
        mime_type_: ?[]const u8,
        unique_key: ?[]const u8,
    ) void {
        {
            const mime_type = if (mime_type_) |m| m else MimeType.byExtension(bun.strings.trimLeadingChar(std.fs.path.extension(source.path.text), '.')).value;
            const contents = source.contents;
            // TODO: make this configurable
            const COPY_THRESHOLD = 128 * 1024; // 128kb
            const should_copy = contents.len >= COPY_THRESHOLD and unique_key != null;
            if (should_copy) return;
            this.url_for_css = url_for_css: {

                // Encode as base64
                const encode_len = bun.base64.encodeLen(contents);
                const data_url_prefix_len = "data:".len + mime_type.len + ";base64,".len;
                const total_buffer_len = data_url_prefix_len + encode_len;
                var encoded = allocator.alloc(u8, total_buffer_len) catch bun.outOfMemory();
                _ = std.fmt.bufPrint(encoded[0..data_url_prefix_len], "data:{s};base64,", .{mime_type}) catch unreachable;
                const len = bun.base64.encode(encoded[data_url_prefix_len..], contents);
                break :url_for_css encoded[0 .. data_url_prefix_len + len];
            };
        }
    }
};

pub const Span = struct {
    text: string = "",
    range: logger.Range = .{},
};

/// This is for TypeScript "enum" and "namespace" blocks. Each block can
/// potentially be instantiated multiple times. The exported members of each
/// block are merged into a single namespace while the non-exported code is
/// still scoped to just within that block:
///
///    let x = 1;
///    namespace Foo {
///      let x = 2;
///      export let y = 3;
///    }
///    namespace Foo {
///      console.log(x); // 1
///      console.log(y); // 3
///    }
///
/// Doing this also works inside an enum:
///
///    enum Foo {
///      A = 3,
///      B = A + 1,
///    }
///    enum Foo {
///      C = A + 2,
///    }
///    console.log(Foo.B) // 4
///    console.log(Foo.C) // 5
///
/// This is a form of identifier lookup that works differently than the
/// hierarchical scope-based identifier lookup in JavaScript. Lookup now needs
/// to search sibling scopes in addition to parent scopes. This is accomplished
/// by sharing the map of exported members between all matching sibling scopes.
pub const TSNamespaceScope = struct {
    /// This is specific to this namespace block. It's the argument of the
    /// immediately-invoked function expression that the namespace block is
    /// compiled into:
    ///
    ///   var ns;
    ///   (function (ns2) {
    ///     ns2.x = 123;
    ///   })(ns || (ns = {}));
    ///
    /// This variable is "ns2" in the above example. It's the symbol to use when
    /// generating property accesses off of this namespace when it's in scope.
    arg_ref: Ref,

    /// This is shared between all sibling namespace blocks
    exported_members: *TSNamespaceMemberMap,

    /// This is a lazily-generated map of identifiers that actually represent
    /// property accesses to this namespace's properties. For example:
    ///
    ///   namespace x {
    ///     export let y = 123
    ///   }
    ///   namespace x {
    ///     export let z = y
    ///   }
    ///
    /// This should be compiled into the following code:
    ///
    ///   var x;
    ///   (function(x2) {
    ///     x2.y = 123;
    ///   })(x || (x = {}));
    ///   (function(x3) {
    ///     x3.z = x3.y;
    ///   })(x || (x = {}));
    ///
    /// When we try to find the symbol "y", we instead return one of these lazily
    /// generated proxy symbols that represent the property access "x3.y". This
    /// map is unique per namespace block because "x3" is the argument symbol that
    /// is specific to that particular namespace block.
    property_accesses: bun.StringArrayHashMapUnmanaged(Ref) = .{},

    /// Even though enums are like namespaces and both enums and namespaces allow
    /// implicit references to properties of sibling scopes, they behave like
    /// separate, er, namespaces. Implicit references only work namespace-to-
    /// namespace and enum-to-enum. They do not work enum-to-namespace. And I'm
    /// not sure what's supposed to happen for the namespace-to-enum case because
    /// the compiler crashes: https://github.com/microsoft/TypeScript/issues/46891.
    /// So basically these both work:
    ///
    ///   enum a { b = 1 }
    ///   enum a { c = b }
    ///
    ///   namespace x { export let y = 1 }
    ///   namespace x { export let z = y }
    ///
    /// This doesn't work:
    ///
    ///   enum a { b = 1 }
    ///   namespace a { export let c = b }
    ///
    /// And this crashes the TypeScript compiler:
    ///
    ///   namespace a { export let b = 1 }
    ///   enum a { c = b }
    ///
    /// Therefore we only allow enum/enum and namespace/namespace interactions.
    is_enum_scope: bool,
};

pub const TSNamespaceMemberMap = bun.StringArrayHashMapUnmanaged(TSNamespaceMember);

pub const TSNamespaceMember = struct {
    loc: logger.Loc,
    data: Data,

    pub const Data = union(enum) {
        /// "namespace ns { export let it }"
        property,
        /// "namespace ns { export namespace it {} }"
        namespace: *TSNamespaceMemberMap,
        /// "enum ns { it }"
        enum_number: f64,
        /// "enum ns { it = 'it' }"
        enum_string: *E.String,
        /// "enum ns { it = something() }"
        enum_property: void,

        pub fn isEnum(data: Data) bool {
            return switch (data) {
                inline else => |_, tag| comptime std.mem.startsWith(u8, @tagName(tag), "enum_"),
            };
        }
    };
};

/// Inlined enum values can only be numbers and strings
/// This type special cases an encoding similar to JSValue, where nan-boxing is used
/// to encode both a 64-bit pointer or a 64-bit float using 64 bits.
pub const InlinedEnumValue = struct {
    raw_data: u64,

    pub const Decoded = union(enum) {
        string: *E.String,
        number: f64,
    };

    /// See JSCJSValue.h in WebKit for more details
    const double_encode_offset = 1 << 49;
    /// See PureNaN.h in WebKit for more details
    const pure_nan: f64 = @bitCast(@as(u64, 0x7ff8000000000000));

    fn purifyNaN(value: f64) f64 {
        return if (std.math.isNan(value)) pure_nan else value;
    }

    pub fn encode(decoded: Decoded) InlinedEnumValue {
        const encoded: InlinedEnumValue = .{ .raw_data = switch (decoded) {
            .string => |ptr| @as(u48, @truncate(@intFromPtr(ptr))),
            .number => |num| @as(u64, @bitCast(purifyNaN(num))) + double_encode_offset,
        } };
        if (Environment.allow_assert) {
            bun.assert(switch (encoded.decode()) {
                .string => |str| str == decoded.string,
                .number => |num| @as(u64, @bitCast(num)) ==
                    @as(u64, @bitCast(purifyNaN(decoded.number))),
            });
        }
        return encoded;
    }

    pub fn decode(encoded: InlinedEnumValue) Decoded {
        if (encoded.raw_data > 0x0000FFFFFFFFFFFF) {
            return .{ .number = @bitCast(encoded.raw_data - double_encode_offset) };
        } else {
            return .{ .string = @ptrFromInt(encoded.raw_data) };
        }
    }
};

pub const ExportsKind = enum {
    // This file doesn't have any kind of export, so it's impossible to say what
    // kind of file this is. An empty file is in this category, for example.
    none,

    // The exports are stored on "module" and/or "exports". Calling "require()"
    // on this module returns "module.exports". All imports to this module are
    // allowed but may return undefined.
    cjs,

    // All export names are known explicitly. Calling "require()" on this module
    // generates an exports object (stored in "exports") with getters for the
    // export names. Named imports to this module are only allowed if they are
    // in the set of export names.
    esm,

    // Some export names are known explicitly, but others fall back to a dynamic
    // run-time object. This is necessary when using the "export * from" syntax
    // with either a CommonJS module or an external module (i.e. a module whose
    // export names are not known at compile-time).
    //
    // Calling "require()" on this module generates an exports object (stored in
    // "exports") with getters for the export names. All named imports to this
    // module are allowed. Direct named imports reference the corresponding export
    // directly. Other imports go through property accesses on "exports".
    esm_with_dynamic_fallback,

    // Like "esm_with_dynamic_fallback", but the module was originally a CommonJS
    // module.
    esm_with_dynamic_fallback_from_cjs,

    pub fn isDynamic(self: ExportsKind) bool {
        return switch (self) {
            .cjs, .esm_with_dynamic_fallback, .esm_with_dynamic_fallback_from_cjs => true,
            .none, .esm => false,
        };
    }

    pub fn isESMWithDynamicFallback(self: ExportsKind) bool {
        return switch (self) {
            .none, .cjs, .esm => false,
            .esm_with_dynamic_fallback, .esm_with_dynamic_fallback_from_cjs => true,
        };
    }

    pub fn jsonStringify(self: @This(), writer: anytype) !void {
        return try writer.write(@tagName(self));
    }
};

pub const DeclaredSymbol = struct {
    ref: Ref,
    is_top_level: bool = false,

    pub const List = struct {
        entries: bun.MultiArrayList(DeclaredSymbol) = .{},

        pub fn refs(this: *const List) []Ref {
            return this.entries.items(.ref);
        }

        pub fn toOwnedSlice(this: *List) List {
            const new = this.*;

            this.* = .{};
            return new;
        }

        pub fn clone(this: *const List, allocator: std.mem.Allocator) !List {
            return List{ .entries = try this.entries.clone(allocator) };
        }

        pub inline fn len(this: List) usize {
            return this.entries.len;
        }

        pub fn append(this: *List, allocator: std.mem.Allocator, entry: DeclaredSymbol) !void {
            try this.ensureUnusedCapacity(allocator, 1);
            this.appendAssumeCapacity(entry);
        }

        pub fn appendList(this: *List, allocator: std.mem.Allocator, other: List) !void {
            try this.ensureUnusedCapacity(allocator, other.len());
            this.appendListAssumeCapacity(other);
        }

        pub fn appendListAssumeCapacity(this: *List, other: List) void {
            this.entries.appendListAssumeCapacity(other.entries);
        }

        pub fn appendAssumeCapacity(this: *List, entry: DeclaredSymbol) void {
            this.entries.appendAssumeCapacity(entry);
        }

        pub fn ensureTotalCapacity(this: *List, allocator: std.mem.Allocator, count: usize) !void {
            try this.entries.ensureTotalCapacity(allocator, count);
        }

        pub fn ensureUnusedCapacity(this: *List, allocator: std.mem.Allocator, count: usize) !void {
            try this.entries.ensureUnusedCapacity(allocator, count);
        }

        pub fn clearRetainingCapacity(this: *List) void {
            this.entries.clearRetainingCapacity();
        }

        pub fn deinit(this: *List, allocator: std.mem.Allocator) void {
            this.entries.deinit(allocator);
        }

        pub fn initCapacity(allocator: std.mem.Allocator, capacity: usize) !List {
            var entries = bun.MultiArrayList(DeclaredSymbol){};
            try entries.ensureUnusedCapacity(allocator, capacity);
            return List{ .entries = entries };
        }

        pub fn fromSlice(allocator: std.mem.Allocator, entries: []const DeclaredSymbol) !List {
            var this = try List.initCapacity(allocator, entries.len);
            errdefer this.deinit(allocator);
            for (entries) |entry| {
                this.appendAssumeCapacity(entry);
            }

            return this;
        }
    };

    fn forEachTopLevelSymbolWithType(decls: *List, comptime Ctx: type, ctx: Ctx, comptime Fn: fn (Ctx, Ref) void) void {
        var entries = decls.entries.slice();
        const is_top_level = entries.items(.is_top_level);
        const refs = entries.items(.ref);

        // TODO: SIMD
        for (is_top_level, refs) |top, ref| {
            if (top) {
                @call(bun.callmod_inline, Fn, .{ ctx, ref });
            }
        }
    }

    pub fn forEachTopLevelSymbol(decls: *List, ctx: anytype, comptime Fn: anytype) void {
        forEachTopLevelSymbolWithType(decls, @TypeOf(ctx), ctx, Fn);
    }
};

pub const Dependency = struct {
    source_index: Index = Index.invalid,
    part_index: Index.Int = 0,

    pub const List = BabyList(Dependency);
};

pub const ExprList = std.ArrayList(Expr);
pub const StmtList = std.ArrayList(Stmt);
pub const BindingList = std.ArrayList(Binding);

// Each file is made up of multiple parts, and each part consists of one or
// more top-level statements. Parts are used for tree shaking and code
// splitting analysis. Individual parts of a file can be discarded by tree
// shaking and can be assigned to separate chunks (i.e. output files) by code
// splitting.
pub const Part = struct {
    pub const ImportRecordIndices = BabyList(u32);
    pub const List = BabyList(Part);

    stmts: []Stmt = &([_]Stmt{}),
    scopes: []*Scope = &([_]*Scope{}),

    /// Each is an index into the file-level import record list
    import_record_indices: ImportRecordIndices = .{},

    /// All symbols that are declared in this part. Note that a given symbol may
    /// have multiple declarations, and so may end up being declared in multiple
    /// parts (e.g. multiple "var" declarations with the same name). Also note
    /// that this list isn't deduplicated and may contain duplicates.
    declared_symbols: DeclaredSymbol.List = .{},

    /// An estimate of the number of uses of all symbols used within this part.
    symbol_uses: SymbolUseMap = .{},

    /// This tracks property accesses off of imported symbols. We don't know
    /// during parsing if an imported symbol is going to be an inlined enum
    /// value or not. This is only known during linking. So we defer adding
    /// a dependency on these imported symbols until we know whether the
    /// property access is an inlined enum value or not.
    import_symbol_property_uses: SymbolPropertyUseMap = .{},

    /// The indices of the other parts in this file that are needed if this part
    /// is needed.
    dependencies: Dependency.List = .{},

    /// If true, this part can be removed if none of the declared symbols are
    /// used. If the file containing this part is imported, then all parts that
    /// don't have this flag enabled must be included.
    can_be_removed_if_unused: bool = false,

    /// This is used for generated parts that we don't want to be present if they
    /// aren't needed. This enables tree shaking for these parts even if global
    /// tree shaking isn't enabled.
    force_tree_shaking: bool = false,

    /// This is true if this file has been marked as live by the tree shaking
    /// algorithm.
    is_live: bool = false,

    tag: Tag = Tag.none,

    pub const Tag = enum {
        none,
        jsx_import,
        runtime,
        cjs_imports,
        react_fast_refresh,
        dirname_filename,
        bun_test,
        dead_due_to_inlining,
        commonjs_named_export,
        import_to_convert_from_require,
    };

    pub const SymbolUseMap = std.ArrayHashMapUnmanaged(Ref, Symbol.Use, RefHashCtx, false);
    pub const SymbolPropertyUseMap = std.ArrayHashMapUnmanaged(Ref, bun.StringHashMapUnmanaged(Symbol.Use), RefHashCtx, false);

    pub fn jsonStringify(self: *const Part, writer: anytype) !void {
        return writer.write(self.stmts);
    }
};

pub const Result = union(enum) {
    already_bundled: AlreadyBundled,
    cached: void,
    ast: Ast,

    pub const AlreadyBundled = enum {
        bun,
        bun_cjs,
        bytecode,
        bytecode_cjs,
    };
};

pub const StmtOrExpr = union(enum) {
    stmt: Stmt,
    expr: Expr,

    pub fn toExpr(stmt_or_expr: StmtOrExpr) Expr {
        return switch (stmt_or_expr) {
            .expr => |expr| expr,
            .stmt => |stmt| switch (stmt.data) {
                .s_function => |s| Expr.init(E.Function, .{ .func = s.func }, stmt.loc),
                .s_class => |s| Expr.init(E.Class, s.class, stmt.loc),
                else => Output.panic("Unexpected statement type in default export: .{s}", .{@tagName(stmt.data)}),
            },
        };
    }
};

pub const NamedImport = struct {
    // Parts within this file that use this import
    local_parts_with_uses: BabyList(u32) = BabyList(u32){},

    alias: ?string,
    alias_loc: ?logger.Loc = null,
    namespace_ref: ?Ref,
    import_record_index: u32,

    // If true, the alias refers to the entire export namespace object of a
    // module. This is no longer represented as an alias called "*" because of
    // the upcoming "Arbitrary module namespace identifier names" feature:
    // https://github.com/tc39/ecma262/pull/2154
    alias_is_star: bool = false,

    // It's useful to flag exported imports because if they are in a TypeScript
    // file, we can't tell if they are a type or a value.
    is_exported: bool = false,
};

pub const NamedExport = struct {
    ref: Ref,
    alias_loc: logger.Loc,
};

pub const StrictModeKind = enum(u4) {
    sloppy_mode,
    explicit_strict_mode,
    implicit_strict_mode_import,
    implicit_strict_mode_export,
    implicit_strict_mode_top_level_await,
    implicit_strict_mode_class,
    pub fn jsonStringify(self: @This(), writer: anytype) !void {
        return try writer.write(@tagName(self));
    }
};

pub const Scope = struct {
    pub const MemberHashMap = bun.StringHashMapUnmanaged(Member);

    id: usize = 0,
    kind: Kind = Kind.block,
    parent: ?*Scope = null,
    children: BabyList(*Scope) = .{},
    members: MemberHashMap = .{},
    generated: BabyList(Ref) = .{},

    // This is used to store the ref of the label symbol for ScopeLabel scopes.
    label_ref: ?Ref = null,
    label_stmt_is_loop: bool = false,

    // If a scope contains a direct eval() expression, then none of the symbols
    // inside that scope can be renamed. We conservatively assume that the
    // evaluated code might reference anything that it has access to.
    contains_direct_eval: bool = false,

    // This is to help forbid "arguments" inside class body scopes
    forbid_arguments: bool = false,

    strict_mode: StrictModeKind = StrictModeKind.sloppy_mode,

    is_after_const_local_prefix: bool = false,

    // This will be non-null if this is a TypeScript "namespace" or "enum"
    ts_namespace: ?*TSNamespaceScope = null,

    pub const NestedScopeMap = std.AutoArrayHashMap(u32, bun.BabyList(*Scope));

    pub fn getMemberHash(name: []const u8) u64 {
        return bun.StringHashMapContext.hash(.{}, name);
    }

    pub fn getMemberWithHash(this: *const Scope, name: []const u8, hash_value: u64) ?Member {
        const hashed = bun.StringHashMapContext.Prehashed{
            .value = hash_value,
            .input = name,
        };
        return this.members.getAdapted(name, hashed);
    }

    pub fn getOrPutMemberWithHash(
        this: *Scope,
        allocator: std.mem.Allocator,
        name: []const u8,
        hash_value: u64,
    ) !MemberHashMap.GetOrPutResult {
        const hashed = bun.StringHashMapContext.Prehashed{
            .value = hash_value,
            .input = name,
        };
        return this.members.getOrPutContextAdapted(allocator, name, hashed, .{});
    }

    pub fn reset(this: *Scope) void {
        this.children.clearRetainingCapacity();
        this.generated.clearRetainingCapacity();
        this.members.clearRetainingCapacity();
        this.parent = null;
        this.id = 0;
        this.label_ref = null;
        this.label_stmt_is_loop = false;
        this.contains_direct_eval = false;
        this.strict_mode = .sloppy_mode;
        this.kind = .block;
    }

    // Do not make this a packed struct
    // Two hours of debugging time lost to that.
    // It causes a crash due to undefined memory
    pub const Member = struct {
        ref: Ref,
        loc: logger.Loc,

        pub fn eql(a: Member, b: Member) bool {
            return @call(bun.callmod_inline, Ref.eql, .{ a.ref, b.ref }) and a.loc.start == b.loc.start;
        }
    };

    pub const SymbolMergeResult = enum {
        forbidden,
        replace_with_new,
        overwrite_with_new,
        keep_existing,
        become_private_get_set_pair,
        become_private_static_get_set_pair,
    };

    pub fn canMergeSymbols(
        scope: *Scope,
        existing: Symbol.Kind,
        new: Symbol.Kind,
        comptime is_typescript_enabled: bool,
    ) SymbolMergeResult {
        if (existing == .unbound) {
            return .replace_with_new;
        }

        if (comptime is_typescript_enabled) {
            // In TypeScript, imports are allowed to silently collide with symbols within
            // the module. Presumably this is because the imports may be type-only:
            //
            //   import {Foo} from 'bar'
            //   class Foo {}
            //
            if (existing == .import) {
                return .replace_with_new;
            }

            // "enum Foo {} enum Foo {}"
            // "namespace Foo { ... } enum Foo {}"
            if (new == .ts_enum and (existing == .ts_enum or existing == .ts_namespace)) {
                return .replace_with_new;
            }

            // "namespace Foo { ... } namespace Foo { ... }"
            // "function Foo() {} namespace Foo { ... }"
            // "enum Foo {} namespace Foo { ... }"
            if (new == .ts_namespace) {
                switch (existing) {
                    .ts_namespace,
                    .ts_enum,
                    .hoisted_function,
                    .generator_or_async_function,
                    .class,
                    => return .keep_existing,
                    else => {},
                }
            }
        }

        // "var foo; var foo;"
        // "var foo; function foo() {}"
        // "function foo() {} var foo;"
        // "function *foo() {} function *foo() {}" but not "{ function *foo() {} function *foo() {} }"
        if (Symbol.isKindHoistedOrFunction(new) and
            Symbol.isKindHoistedOrFunction(existing) and
            (scope.kind == .entry or scope.kind == .function_body or scope.kind == .function_args or
                (new == existing and Symbol.isKindHoisted(existing))))
        {
            return .replace_with_new;
        }

        // "get #foo() {} set #foo() {}"
        // "set #foo() {} get #foo() {}"
        if ((existing == .private_get and new == .private_set) or
            (existing == .private_set and new == .private_get))
        {
            return .become_private_get_set_pair;
        }
        if ((existing == .private_static_get and new == .private_static_set) or
            (existing == .private_static_set and new == .private_static_get))
        {
            return .become_private_static_get_set_pair;
        }

        // "try {} catch (e) { var e }"
        if (existing == .catch_identifier and new == .hoisted) {
            return .replace_with_new;
        }

        // "function() { var arguments }"
        if (existing == .arguments and new == .hoisted) {
            return .keep_existing;
        }

        // "function() { let arguments }"
        if (existing == .arguments and new != .hoisted) {
            return .overwrite_with_new;
        }

        return .forbidden;
    }

    pub const Kind = enum(u8) {
        block,
        with,
        label,
        class_name,
        class_body,
        catch_binding,

        // The scopes below stop hoisted variables from extending into parent scopes
        entry, // This is a module, TypeScript enum, or TypeScript namespace
        function_args,
        function_body,
        class_static_init,

        pub fn jsonStringify(self: @This(), writer: anytype) !void {
            return try writer.write(@tagName(self));
        }
    };

    pub fn recursiveSetStrictMode(s: *Scope, kind: StrictModeKind) void {
        if (s.strict_mode == .sloppy_mode) {
            s.strict_mode = kind;
            for (s.children.slice()) |child| {
                child.recursiveSetStrictMode(kind);
            }
        }
    }

    pub inline fn kindStopsHoisting(s: *const Scope) bool {
        return @intFromEnum(s.kind) >= @intFromEnum(Kind.entry);
    }
};

pub fn printmem(comptime format: string, args: anytype) void {
    defer Output.flush();
    Output.initTest();
    Output.print(format, args);
}

pub const Macro = struct {
    const JavaScript = bun.JSC;
    const Resolver = @import("./resolver/resolver.zig").Resolver;
    const isPackagePath = @import("./resolver/resolver.zig").isPackagePath;
    const ResolveResult = @import("./resolver/resolver.zig").Result;
    const DotEnv = @import("./env_loader.zig");
    const js = @import("./bun.js/javascript_core_c_api.zig");
    const Transpiler = bun.Transpiler;
    const MacroEntryPoint = bun.transpiler.EntryPoints.MacroEntryPoint;
    const MacroRemap = @import("./resolver/package_json.zig").MacroMap;
    pub const MacroRemapEntry = @import("./resolver/package_json.zig").MacroImportReplacementMap;

    pub const namespace: string = "macro";
    pub const namespaceWithColon: string = namespace ++ ":";

    pub fn isMacroPath(str: string) bool {
        return strings.hasPrefixComptime(str, namespaceWithColon);
    }

    pub const MacroContext = struct {
        pub const MacroMap = std.AutoArrayHashMap(i32, Macro);

        resolver: *Resolver,
        env: *DotEnv.Loader,
        macros: MacroMap,
        remap: MacroRemap,
        javascript_object: JSC.JSValue = JSC.JSValue.zero,

        pub fn getRemap(this: MacroContext, path: string) ?MacroRemapEntry {
            if (this.remap.entries.len == 0) return null;
            return this.remap.get(path);
        }

        pub fn init(transpiler: *Transpiler) MacroContext {
            return MacroContext{
                .macros = MacroMap.init(default_allocator),
                .resolver = &transpiler.resolver,
                .env = transpiler.env,
                .remap = transpiler.options.macro_remap,
            };
        }

        pub fn call(
            this: *MacroContext,
            import_record_path: string,
            source_dir: string,
            log: *logger.Log,
            source: *const logger.Source,
            import_range: logger.Range,
            caller: Expr,
            function_name: string,
        ) anyerror!Expr {
            Expr.Data.Store.disable_reset = true;
            Stmt.Data.Store.disable_reset = true;
            defer Expr.Data.Store.disable_reset = false;
            defer Stmt.Data.Store.disable_reset = false;
            // const is_package_path = isPackagePath(specifier);
            const import_record_path_without_macro_prefix = if (isMacroPath(import_record_path))
                import_record_path[namespaceWithColon.len..]
            else
                import_record_path;

            bun.assert(!isMacroPath(import_record_path_without_macro_prefix));

            const input_specifier = brk: {
                if (JSC.ModuleLoader.HardcodedModule.Alias.get(import_record_path, .bun)) |replacement| {
                    break :brk replacement.path;
                }

                const resolve_result = this.resolver.resolve(source_dir, import_record_path_without_macro_prefix, .stmt) catch |err| {
                    switch (err) {
                        error.ModuleNotFound => {
                            log.addResolveError(
                                source,
                                import_range,
                                log.msgs.allocator,
                                "Macro \"{s}\" not found",
                                .{import_record_path},
                                .stmt,
                                err,
                            ) catch unreachable;
                            return error.MacroNotFound;
                        },
                        else => {
                            log.addRangeErrorFmt(
                                source,
                                import_range,
                                log.msgs.allocator,
                                "{s} resolving macro \"{s}\"",
                                .{ @errorName(err), import_record_path },
                            ) catch unreachable;
                            return err;
                        },
                    }
                };
                break :brk resolve_result.path_pair.primary.text;
            };

            var specifier_buf: [64]u8 = undefined;
            var specifier_buf_len: u32 = 0;
            const hash = MacroEntryPoint.generateID(
                input_specifier,
                function_name,
                &specifier_buf,
                &specifier_buf_len,
            );

            const macro_entry = this.macros.getOrPut(hash) catch unreachable;
            if (!macro_entry.found_existing) {
                macro_entry.value_ptr.* = Macro.init(
                    default_allocator,
                    this.resolver,
                    input_specifier,
                    log,
                    this.env,
                    function_name,
                    specifier_buf[0..specifier_buf_len],
                    hash,
                ) catch |err| {
                    macro_entry.value_ptr.* = Macro{ .resolver = undefined, .disabled = true };
                    return err;
                };
                Output.flush();
            }
            defer Output.flush();

            const macro = macro_entry.value_ptr.*;
            if (macro.disabled) {
                return caller;
            }
            macro.vm.enableMacroMode();
            defer macro.vm.disableMacroMode();

            const Wrapper = struct {
                args: std.meta.ArgsTuple(@TypeOf(Macro.Runner.run)),
                ret: Runner.MacroError!Expr,

                pub fn call(self: *@This()) void {
                    self.ret = @call(.auto, Macro.Runner.run, self.args);
                }
            };
            var wrapper = Wrapper{
                .args = .{
                    macro,
                    log,
                    default_allocator,
                    function_name,
                    caller,
                    source,
                    hash,
                    this.javascript_object,
                },
                .ret = undefined,
            };

            macro.vm.runWithAPILock(Wrapper, &wrapper, Wrapper.call);
            return try wrapper.ret;
            // this.macros.getOrPut(key: K)
        }
    };

    pub const MacroResult = struct {
        import_statements: []S.Import = &[_]S.Import{},
        replacement: Expr,
    };

    resolver: *Resolver,
    vm: *JavaScript.VirtualMachine = undefined,

    resolved: ResolveResult = undefined,
    disabled: bool = false,

    pub fn init(
        _: std.mem.Allocator,
        resolver: *Resolver,
        input_specifier: []const u8,
        log: *logger.Log,
        env: *DotEnv.Loader,
        function_name: string,
        specifier: string,
        hash: i32,
    ) !Macro {
        var vm: *JavaScript.VirtualMachine = if (JavaScript.VirtualMachine.isLoaded())
            JavaScript.VirtualMachine.get()
        else brk: {
            const old_transform_options = resolver.opts.transform_options;
            defer resolver.opts.transform_options = old_transform_options;

            // JSC needs to be initialized if building from CLI
            JSC.initialize(false);

            var _vm = try JavaScript.VirtualMachine.init(.{
                .allocator = default_allocator,
                .args = resolver.opts.transform_options,
                .log = log,
                .is_main_thread = false,
                .env_loader = env,
            });

            _vm.enableMacroMode();
            _vm.eventLoop().ensureWaker();

            try _vm.transpiler.configureDefines();
            break :brk _vm;
        };

        vm.enableMacroMode();

        const loaded_result = try vm.loadMacroEntryPoint(input_specifier, function_name, specifier, hash);

        switch (loaded_result.unwrap(vm.jsc, .leave_unhandled)) {
            .rejected => |result| {
                _ = vm.unhandledRejection(vm.global, result, loaded_result.asValue());
                vm.disableMacroMode();
                return error.MacroLoadError;
            },
            else => {},
        }

        return Macro{
            .vm = vm,
            .resolver = resolver,
        };
    }

    pub const Runner = struct {
        const VisitMap = std.AutoHashMapUnmanaged(JSC.JSValue, Expr);

        threadlocal var args_buf: [3]js.JSObjectRef = undefined;
        threadlocal var exception_holder: JSC.ZigException.Holder = undefined;
        pub const MacroError = error{ MacroFailed, OutOfMemory } || ToJSError || bun.JSError;

        pub const Run = struct {
            caller: Expr,
            function_name: string,
            macro: *const Macro,
            global: *JSC.JSGlobalObject,
            allocator: std.mem.Allocator,
            id: i32,
            log: *logger.Log,
            source: *const logger.Source,
            visited: VisitMap = VisitMap{},
            is_top_level: bool = false,

            pub fn runAsync(
                macro: Macro,
                log: *logger.Log,
                allocator: std.mem.Allocator,
                function_name: string,
                caller: Expr,
                args: []JSC.JSValue,
                source: *const logger.Source,
                id: i32,
            ) MacroError!Expr {
                const macro_callback = macro.vm.macros.get(id) orelse return caller;

                const result = js.JSObjectCallAsFunctionReturnValueHoldingAPILock(
                    macro.vm.global,
                    macro_callback,
                    null,
                    args.len,
                    @as([*]js.JSObjectRef, @ptrCast(args.ptr)),
                );

                var runner = Run{
                    .caller = caller,
                    .function_name = function_name,
                    .macro = &macro,
                    .allocator = allocator,
                    .global = macro.vm.global,
                    .id = id,
                    .log = log,
                    .source = source,
                    .visited = VisitMap{},
                };

                defer runner.visited.deinit(allocator);

                return try runner.run(
                    result,
                );
            }

            pub fn run(
                this: *Run,
                value: JSC.JSValue,
            ) MacroError!Expr {
                return switch (JSC.ConsoleObject.Formatter.Tag.get(value, this.global).tag) {
                    .Error => this.coerce(value, .Error),
                    .Undefined => this.coerce(value, .Undefined),
                    .Null => this.coerce(value, .Null),
                    .Private => this.coerce(value, .Private),
                    .Boolean => this.coerce(value, .Boolean),
                    .Array => this.coerce(value, .Array),
                    .Object => this.coerce(value, .Object),
                    .toJSON, .JSON => this.coerce(value, .JSON),
                    .Integer => this.coerce(value, .Integer),
                    .Double => this.coerce(value, .Double),
                    .String => this.coerce(value, .String),
                    .Promise => this.coerce(value, .Promise),
                    else => brk: {
                        const name = value.getClassInfoName() orelse "unknown";

                        this.log.addErrorFmt(
                            this.source,
                            this.caller.loc,
                            this.allocator,
                            "cannot coerce {s} ({s}) to Bun's AST. Please return a simpler type",
                            .{ name, @tagName(value.jsType()) },
                        ) catch unreachable;
                        break :brk error.MacroFailed;
                    },
                };
            }

            pub fn coerce(
                this: *Run,
                value: JSC.JSValue,
                comptime tag: JSC.ConsoleObject.Formatter.Tag,
            ) MacroError!Expr {
                switch (comptime tag) {
                    .Error => {
                        _ = this.macro.vm.uncaughtException(this.global, value, false);
                        return this.caller;
                    },
                    .Undefined => if (this.is_top_level)
                        return this.caller
                    else
                        return Expr.init(E.Undefined, E.Undefined{}, this.caller.loc),
                    .Null => return Expr.init(E.Null, E.Null{}, this.caller.loc),
                    .Private => {
                        this.is_top_level = false;
                        const _entry = this.visited.getOrPut(this.allocator, value) catch unreachable;
                        if (_entry.found_existing) {
                            return _entry.value_ptr.*;
                        }

                        var blob_: ?JSC.WebCore.Blob = null;
                        const mime_type: ?MimeType = null;

                        if (value.jsType() == .DOMWrapper) {
                            if (value.as(JSC.WebCore.Response)) |resp| {
                                return this.run(try resp.getBlobWithoutCallFrame(this.global));
                            } else if (value.as(JSC.WebCore.Request)) |resp| {
                                return this.run(try resp.getBlobWithoutCallFrame(this.global));
                            } else if (value.as(JSC.WebCore.Blob)) |resp| {
                                blob_ = resp.*;
                                blob_.?.allocator = null;
                            } else if (value.as(bun.api.ResolveMessage) != null or value.as(bun.api.BuildMessage) != null) {
                                _ = this.macro.vm.uncaughtException(this.global, value, false);
                                return error.MacroFailed;
                            }
                        }

                        if (blob_) |*blob| {
                            const out_expr = Expr.fromBlob(
                                blob,
                                this.allocator,
                                mime_type,
                                this.log,
                                this.caller.loc,
                            ) catch {
                                blob.deinit();
                                return error.MacroFailed;
                            };
                            if (out_expr.data == .e_string) {
                                blob.deinit();
                            }

                            return out_expr;
                        }

                        return Expr.init(E.String, E.String.empty, this.caller.loc);
                    },

                    .Boolean => {
                        return Expr{ .data = .{ .e_boolean = .{ .value = value.toBoolean() } }, .loc = this.caller.loc };
                    },
                    JSC.ConsoleObject.Formatter.Tag.Array => {
                        this.is_top_level = false;

                        const _entry = this.visited.getOrPut(this.allocator, value) catch unreachable;
                        if (_entry.found_existing) {
                            switch (_entry.value_ptr.*.data) {
                                .e_object, .e_array => {
                                    this.log.addErrorFmt(this.source, this.caller.loc, this.allocator, "converting circular structure to Bun AST is not implemented yet", .{}) catch unreachable;
                                    return error.MacroFailed;
                                },
                                else => {},
                            }
                            return _entry.value_ptr.*;
                        }

                        var iter = JSC.JSArrayIterator.init(value, this.global);
                        if (iter.len == 0) {
                            const result = Expr.init(
                                E.Array,
                                E.Array{
                                    .items = ExprNodeList.init(&[_]Expr{}),
                                    .was_originally_macro = true,
                                },
                                this.caller.loc,
                            );
                            _entry.value_ptr.* = result;
                            return result;
                        }
                        var array = this.allocator.alloc(Expr, iter.len) catch unreachable;
                        var out = Expr.init(
                            E.Array,
                            E.Array{
                                .items = ExprNodeList.init(array[0..0]),
                                .was_originally_macro = true,
                            },
                            this.caller.loc,
                        );
                        _entry.value_ptr.* = out;

                        errdefer this.allocator.free(array);
                        var i: usize = 0;
                        while (iter.next()) |item| {
                            array[i] = try this.run(item);
                            if (array[i].isMissing())
                                continue;
                            i += 1;
                        }
                        out.data.e_array.items = ExprNodeList.init(array);
                        _entry.value_ptr.* = out;
                        return out;
                    },
                    // TODO: optimize this
                    JSC.ConsoleObject.Formatter.Tag.Object => {
                        this.is_top_level = false;
                        const _entry = this.visited.getOrPut(this.allocator, value) catch unreachable;
                        if (_entry.found_existing) {
                            switch (_entry.value_ptr.*.data) {
                                .e_object, .e_array => {
                                    this.log.addErrorFmt(this.source, this.caller.loc, this.allocator, "converting circular structure to Bun AST is not implemented yet", .{}) catch unreachable;
                                    return error.MacroFailed;
                                },
                                else => {},
                            }
                            return _entry.value_ptr.*;
                        }
                        // SAFETY: tag ensures `value` is an object.
                        const obj = value.getObject() orelse unreachable;
                        var object_iter = try JSC.JSPropertyIterator(.{
                            .skip_empty_name = false,
                            .include_value = true,
                        }).init(this.global, obj);
                        defer object_iter.deinit();
                        var properties = this.allocator.alloc(G.Property, object_iter.len) catch unreachable;
                        errdefer this.allocator.free(properties);
                        var out = Expr.init(
                            E.Object,
                            E.Object{
                                .properties = BabyList(G.Property).init(properties),
                                .was_originally_macro = true,
                            },
                            this.caller.loc,
                        );
                        _entry.value_ptr.* = out;

                        while (try object_iter.next()) |prop| {
                            properties[object_iter.i] = G.Property{
                                .key = Expr.init(E.String, E.String.init(prop.toOwnedSlice(this.allocator) catch unreachable), this.caller.loc),
                                .value = try this.run(object_iter.value),
                            };
                        }
                        out.data.e_object.properties = BabyList(G.Property).init(properties[0..object_iter.i]);
                        _entry.value_ptr.* = out;
                        return out;
                    },

                    .JSON => {
                        this.is_top_level = false;
                        // if (console_tag.cell == .JSDate) {
                        //     // in the code for printing dates, it never exceeds this amount
                        //     var iso_string_buf = this.allocator.alloc(u8, 36) catch unreachable;
                        //     var str = JSC.ZigString.init("");
                        //     value.jsonStringify(this.global, 0, &str);
                        //     var out_buf: []const u8 = std.fmt.bufPrint(iso_string_buf, "{}", .{str}) catch "";
                        //     if (out_buf.len > 2) {
                        //         // trim the quotes
                        //         out_buf = out_buf[1 .. out_buf.len - 1];
                        //     }
                        //     return Expr.init(E.New, E.New{.target = Expr.init(E.Dot{.target = E}) })
                        // }
                    },

                    .Integer => {
                        return Expr.init(E.Number, E.Number{ .value = @as(f64, @floatFromInt(value.toInt32())) }, this.caller.loc);
                    },
                    .Double => {
                        return Expr.init(E.Number, E.Number{ .value = value.asNumber() }, this.caller.loc);
                    },
                    .String => {
                        var bun_str = try value.toBunString(this.global);
                        defer bun_str.deref();

                        // encode into utf16 so the printer escapes the string correctly
                        var utf16_bytes = this.allocator.alloc(u16, bun_str.length()) catch unreachable;
                        const out_slice = utf16_bytes[0 .. (bun_str.encodeInto(std.mem.sliceAsBytes(utf16_bytes), .utf16le) catch 0) / 2];
                        return Expr.init(E.String, E.String.init(out_slice), this.caller.loc);
                    },
                    .Promise => {
                        const _entry = this.visited.getOrPut(this.allocator, value) catch unreachable;
                        if (_entry.found_existing) {
                            return _entry.value_ptr.*;
                        }

                        const promise = value.asAnyPromise() orelse @panic("Unexpected promise type");

                        this.macro.vm.waitForPromise(promise);

                        const promise_result = promise.result(this.macro.vm.jsc);
                        const rejected = promise.status(this.macro.vm.jsc) == .rejected;

                        if (promise_result.isUndefined() and this.is_top_level) {
                            this.is_top_level = false;
                            return this.caller;
                        }

                        if (rejected or promise_result.isError() or promise_result.isAggregateError(this.global) or promise_result.isException(this.global.vm())) {
                            _ = this.macro.vm.unhandledRejection(this.global, promise_result, promise.asValue());
                            return error.MacroFailed;
                        }
                        this.is_top_level = false;
                        const result = try this.run(promise_result);

                        _entry.value_ptr.* = result;
                        return result;
                    },
                    else => {},
                }

                this.log.addErrorFmt(
                    this.source,
                    this.caller.loc,
                    this.allocator,
                    "cannot coerce {s} to Bun's AST. Please return a simpler type",
                    .{@tagName(value.jsType())},
                ) catch unreachable;
                return error.MacroFailed;
            }
        };

        pub fn run(
            macro: Macro,
            log: *logger.Log,
            allocator: std.mem.Allocator,
            function_name: string,
            caller: Expr,
            source: *const logger.Source,
            id: i32,
            javascript_object: JSC.JSValue,
        ) MacroError!Expr {
            if (comptime Environment.isDebug) Output.prettyln("<r><d>[macro]<r> call <d><b>{s}<r>", .{function_name});

            exception_holder = JSC.ZigException.Holder.init();
            var js_args: []JSC.JSValue = &.{};
            var js_processed_args_len: usize = 0;
            defer {
                for (js_args[0..js_processed_args_len -| @as(usize, @intFromBool(javascript_object != .zero))]) |arg| {
                    arg.unprotect();
                }

                allocator.free(js_args);
            }

            const globalObject = JSC.VirtualMachine.get().global;

            switch (caller.data) {
                .e_call => |call| {
                    const call_args: []Expr = call.args.slice();
                    js_args = try allocator.alloc(JSC.JSValue, call_args.len + @as(usize, @intFromBool(javascript_object != .zero)));
                    js_processed_args_len = js_args.len;

                    for (0.., call_args, js_args[0..call_args.len]) |i, in, *out| {
                        const value = in.toJS(
                            allocator,
                            globalObject,
                        ) catch |e| {
                            // Keeping a separate variable instead of modifying js_args.len
                            // due to allocator.free call in defer
                            js_processed_args_len = i;
                            return e;
                        };
                        value.protect();
                        out.* = value;
                    }
                },
                .e_template => {
                    @panic("TODO: support template literals in macros");
                },
                else => {
                    @panic("Unexpected caller type");
                },
            }

            if (javascript_object != .zero) {
                if (js_args.len == 0) {
                    js_args = try allocator.alloc(JSC.JSValue, 1);
                }

                js_args[js_args.len - 1] = javascript_object;
            }

            const CallFunction = @TypeOf(Run.runAsync);
            const CallArgs = std.meta.ArgsTuple(CallFunction);
            const CallData = struct {
                threadlocal var call_args: CallArgs = undefined;
                threadlocal var result: MacroError!Expr = undefined;
                pub fn callWrapper(args: CallArgs) MacroError!Expr {
                    JSC.markBinding(@src());
                    call_args = args;
                    Bun__startMacro(&call, JSC.VirtualMachine.get().global);
                    return result;
                }

                pub fn call() callconv(.C) void {
                    const call_args_copy = call_args;
                    const local_result = @call(.auto, Run.runAsync, call_args_copy);
                    result = local_result;
                }
            };

            // TODO: can change back to `return CallData.callWrapper(.{`
            // when https://github.com/ziglang/zig/issues/16242 is fixed
            return CallData.callWrapper(CallArgs{
                macro,
                log,
                allocator,
                function_name,
                caller,
                js_args,
                source,
                id,
            });
        }

        extern "c" fn Bun__startMacro(function: *const anyopaque, *anyopaque) void;
    };
};

pub const ASTMemoryAllocator = struct {
    const SFA = std.heap.StackFallbackAllocator(@min(8192, std.heap.page_size_min));

    stack_allocator: SFA = undefined,
    bump_allocator: std.mem.Allocator = undefined,
    allocator: std.mem.Allocator,
    previous: ?*ASTMemoryAllocator = null,

    pub fn enter(this: *ASTMemoryAllocator, allocator: std.mem.Allocator) ASTMemoryAllocator.Scope {
        this.allocator = allocator;
        this.stack_allocator = SFA{
            .buffer = undefined,
            .fallback_allocator = allocator,
            .fixed_buffer_allocator = undefined,
        };
        this.bump_allocator = this.stack_allocator.get();
        this.previous = null;
        var ast_scope = ASTMemoryAllocator.Scope{
            .current = this,
            .previous = Stmt.Data.Store.memory_allocator,
        };
        ast_scope.enter();
        return ast_scope;
    }
    pub const Scope = struct {
        current: ?*ASTMemoryAllocator = null,
        previous: ?*ASTMemoryAllocator = null,

        pub fn enter(this: *@This()) void {
            bun.debugAssert(Expr.Data.Store.memory_allocator == Stmt.Data.Store.memory_allocator);

            this.previous = Expr.Data.Store.memory_allocator;

            const current = this.current;

            Expr.Data.Store.memory_allocator = current;
            Stmt.Data.Store.memory_allocator = current;

            if (current == null) {
                Stmt.Data.Store.begin();
                Expr.Data.Store.begin();
            }
        }

        pub fn exit(this: *const @This()) void {
            Expr.Data.Store.memory_allocator = this.previous;
            Stmt.Data.Store.memory_allocator = this.previous;
        }
    };

    pub fn reset(this: *ASTMemoryAllocator) void {
        this.stack_allocator = SFA{
            .buffer = undefined,
            .fallback_allocator = this.allocator,
            .fixed_buffer_allocator = undefined,
        };
        this.bump_allocator = this.stack_allocator.get();
    }

    pub fn push(this: *ASTMemoryAllocator) void {
        Stmt.Data.Store.memory_allocator = this;
        Expr.Data.Store.memory_allocator = this;
    }

    pub fn pop(this: *ASTMemoryAllocator) void {
        const prev = this.previous;
        bun.assert(prev != this);
        Stmt.Data.Store.memory_allocator = prev;
        Expr.Data.Store.memory_allocator = prev;
        this.previous = null;
    }

    pub fn append(this: ASTMemoryAllocator, comptime ValueType: type, value: anytype) *ValueType {
        const ptr = this.bump_allocator.create(ValueType) catch unreachable;
        ptr.* = value;
        return ptr;
    }

    /// Initialize ASTMemoryAllocator as `undefined`, and call this.
    pub fn initWithoutStack(this: *ASTMemoryAllocator, arena: std.mem.Allocator) void {
        this.stack_allocator = SFA{
            .buffer = undefined,
            .fallback_allocator = arena,
            .fixed_buffer_allocator = .init(&.{}),
        };
        this.bump_allocator = this.stack_allocator.get();
    }
};

pub const UseDirective = enum(u2) {
    // TODO: Remove this, and provide `UseDirective.Optional` instead
    none,
    /// "use client"
    client,
    /// "use server"
    server,

    pub const Boundering = enum(u2) {
        client = @intFromEnum(UseDirective.client),
        server = @intFromEnum(UseDirective.server),
    };

    pub const Flags = struct {
        has_any_client: bool = false,
    };

    pub fn isBoundary(this: UseDirective, other: UseDirective) bool {
        if (this == other or other == .none)
            return false;

        return true;
    }

    pub fn boundering(this: UseDirective, other: UseDirective) ?Boundering {
        if (this == other or other == .none)
            return null;
        return @enumFromInt(@intFromEnum(other));
    }

    pub fn parse(contents: []const u8) ?UseDirective {
        const truncated = std.mem.trimLeft(u8, contents, " \t\n\r;");

        if (truncated.len < "'use client';".len)
            return .none;

        const directive_string = truncated[0.."'use client';".len].*;

        const first_quote = directive_string[0];
        const last_quote = directive_string[directive_string.len - 2];
        if (first_quote != last_quote or (first_quote != '"' and first_quote != '\'' and first_quote != '`'))
            return .none;

        const unquoted = directive_string[1 .. directive_string.len - 2];

        if (strings.eqlComptime(unquoted, "use client")) {
            return .client;
        }

        if (strings.eqlComptime(unquoted, "use server")) {
            return .server;
        }

        return null;
    }
};

/// Represents a boundary between client and server code. Every boundary
/// gets bundled twice, once for the desired target, and once to generate
/// a module of "references". Specifically, the generated file takes the
/// canonical Ast as input to derive a wrapper. See `Framework.ServerComponents`
/// for more details about this generated file.
///
/// This is sometimes abbreviated as SCB
pub const ServerComponentBoundary = struct {
    use_directive: UseDirective,

    /// The index of the original file.
    source_index: Index.Int,

    /// Index to the file imported on the opposite platform, which is
    /// generated by the bundler. For client components, this is the
    /// server's code. For server actions, this is the client's code.
    reference_source_index: Index.Int,

    /// When `bake.Framework.ServerComponents.separate_ssr_graph` is enabled this
    /// points to the separated module. When the SSR graph is not separate, this is
    /// equal to `reference_source_index`
    //
    // TODO: Is this used for server actions.
    ssr_source_index: Index.Int,

    /// The requirements for this data structure is to have reasonable lookup
    /// speed, but also being able to pull a `[]const Index.Int` of all
    /// boundaries for iteration.
    pub const List = struct {
        list: std.MultiArrayList(ServerComponentBoundary) = .{},
        /// Used to facilitate fast lookups into `items` by `.source_index`
        map: Map = .{},

        const Map = std.ArrayHashMapUnmanaged(void, void, struct {}, true);

        /// Can only be called on the bundler thread.
        pub fn put(
            m: *List,
            allocator: std.mem.Allocator,
            source_index: Index.Int,
            use_directive: UseDirective,
            reference_source_index: Index.Int,
            ssr_source_index: Index.Int,
        ) !void {
            try m.list.append(allocator, .{
                .source_index = source_index,
                .use_directive = use_directive,
                .reference_source_index = reference_source_index,
                .ssr_source_index = ssr_source_index,
            });
            const gop = try m.map.getOrPutAdapted(
                allocator,
                source_index,
                Adapter{ .list = m.list.slice() },
            );
            bun.assert(!gop.found_existing);
        }

        /// Can only be called on the bundler thread.
        pub fn getIndex(l: *const List, real_source_index: Index.Int) ?usize {
            return l.map.getIndexAdapted(
                real_source_index,
                Adapter{ .list = l.list.slice() },
            );
        }

        /// Use this to improve speed of accessing fields at the cost of
        /// storing more pointers. Invalidated when input is mutated.
        pub fn slice(l: List) Slice {
            return .{ .list = l.list.slice(), .map = l.map };
        }

        pub const Slice = struct {
            list: std.MultiArrayList(ServerComponentBoundary).Slice,
            map: Map,

            pub fn getIndex(l: *const Slice, real_source_index: Index.Int) ?usize {
                return l.map.getIndexAdapted(
                    real_source_index,
                    Adapter{ .list = l.list },
                ) orelse return null;
            }

            pub fn getReferenceSourceIndex(l: *const Slice, real_source_index: Index.Int) ?u32 {
                const i = l.map.getIndexAdapted(
                    real_source_index,
                    Adapter{ .list = l.list },
                ) orelse return null;
                bun.unsafeAssert(l.list.capacity > 0); // optimize MultiArrayList.Slice.items
                return l.list.items(.reference_source_index)[i];
            }

            pub fn bitSet(scbs: Slice, alloc: std.mem.Allocator, input_file_count: usize) !bun.bit_set.DynamicBitSetUnmanaged {
                var scb_bitset = try bun.bit_set.DynamicBitSetUnmanaged.initEmpty(alloc, input_file_count);
                for (scbs.list.items(.source_index)) |source_index| {
                    scb_bitset.set(source_index);
                }
                return scb_bitset;
            }
        };

        pub const Adapter = struct {
            list: std.MultiArrayList(ServerComponentBoundary).Slice,

            pub fn hash(_: Adapter, key: Index.Int) u32 {
                return std.hash.uint32(key);
            }

            pub fn eql(adapt: Adapter, a: Index.Int, _: void, b_index: usize) bool {
                bun.unsafeAssert(adapt.list.capacity > 0); // optimize MultiArrayList.Slice.items
                return a == adapt.list.items(.source_index)[b_index];
            }
        };
    };
};

extern fn JSC__jsToNumber(latin1_ptr: [*]const u8, len: usize) f64;

fn stringToEquivalentNumberValue(str: []const u8) f64 {
    // +"" -> 0
    if (str.len == 0) return 0;
    if (!bun.strings.isAllASCII(str))
        return std.math.nan(f64);
    return JSC__jsToNumber(str.ptr, str.len);
}

// test "Binding.init" {
//     var binding = Binding.alloc(
//         std.heap.page_allocator,
//         B.Identifier{ .ref = Ref{ .source_index = 0, .innerIndex() = 10 } },
//         logger.Loc{ .start = 1 },
//     );
//     std.testing.expect(binding.loc.start == 1);
//     std.testing.expect(@as(Binding.Tag, binding.data) == Binding.Tag.b_identifier);

//     printmem("-------Binding:           {d} bits\n", .{@bitSizeOf(Binding)});
//     printmem("B.Identifier:             {d} bits\n", .{@bitSizeOf(B.Identifier)});
//     printmem("B.Array:                  {d} bits\n", .{@bitSizeOf(B.Array)});
//     printmem("B.Property:               {d} bits\n", .{@bitSizeOf(B.Property)});
//     printmem("B.Object:                 {d} bits\n", .{@bitSizeOf(B.Object)});
//     printmem("B.Missing:                {d} bits\n", .{@bitSizeOf(B.Missing)});
//     printmem("-------Binding:           {d} bits\n", .{@bitSizeOf(Binding)});
// }

// test "Stmt.init" {
//     var stmt = Stmt.alloc(
//         std.heap.page_allocator,
//         S.Continue{},
//         logger.Loc{ .start = 1 },
//     );
//     std.testing.expect(stmt.loc.start == 1);
//     std.testing.expect(@as(Stmt.Tag, stmt.data) == Stmt.Tag.s_continue);

//     printmem("-----Stmt       {d} bits\n", .{@bitSizeOf(Stmt)});
//     printmem("StmtNodeList:   {d} bits\n", .{@bitSizeOf(StmtNodeList)});
//     printmem("StmtOrExpr:     {d} bits\n", .{@bitSizeOf(StmtOrExpr)});
//     printmem("S.Block         {d} bits\n", .{@bitSizeOf(S.Block)});
//     printmem("S.Comment       {d} bits\n", .{@bitSizeOf(S.Comment)});
//     printmem("S.Directive     {d} bits\n", .{@bitSizeOf(S.Directive)});
//     printmem("S.ExportClause  {d} bits\n", .{@bitSizeOf(S.ExportClause)});
//     printmem("S.Empty         {d} bits\n", .{@bitSizeOf(S.Empty)});
//     printmem("S.TypeScript    {d} bits\n", .{@bitSizeOf(S.TypeScript)});
//     printmem("S.Debugger      {d} bits\n", .{@bitSizeOf(S.Debugger)});
//     printmem("S.ExportFrom    {d} bits\n", .{@bitSizeOf(S.ExportFrom)});
//     printmem("S.ExportDefault {d} bits\n", .{@bitSizeOf(S.ExportDefault)});
//     printmem("S.Enum          {d} bits\n", .{@bitSizeOf(S.Enum)});
//     printmem("S.Namespace     {d} bits\n", .{@bitSizeOf(S.Namespace)});
//     printmem("S.Function      {d} bits\n", .{@bitSizeOf(S.Function)});
//     printmem("S.Class         {d} bits\n", .{@bitSizeOf(S.Class)});
//     printmem("S.If            {d} bits\n", .{@bitSizeOf(S.If)});
//     printmem("S.For           {d} bits\n", .{@bitSizeOf(S.For)});
//     printmem("S.ForIn         {d} bits\n", .{@bitSizeOf(S.ForIn)});
//     printmem("S.ForOf         {d} bits\n", .{@bitSizeOf(S.ForOf)});
//     printmem("S.DoWhile       {d} bits\n", .{@bitSizeOf(S.DoWhile)});
//     printmem("S.While         {d} bits\n", .{@bitSizeOf(S.While)});
//     printmem("S.With          {d} bits\n", .{@bitSizeOf(S.With)});
//     printmem("S.Try           {d} bits\n", .{@bitSizeOf(S.Try)});
//     printmem("S.Switch        {d} bits\n", .{@bitSizeOf(S.Switch)});
//     printmem("S.Import        {d} bits\n", .{@bitSizeOf(S.Import)});
//     printmem("S.Return        {d} bits\n", .{@bitSizeOf(S.Return)});
//     printmem("S.Throw         {d} bits\n", .{@bitSizeOf(S.Throw)});
//     printmem("S.Local         {d} bits\n", .{@bitSizeOf(S.Local)});
//     printmem("S.Break         {d} bits\n", .{@bitSizeOf(S.Break)});
//     printmem("S.Continue      {d} bits\n", .{@bitSizeOf(S.Continue)});
//     printmem("-----Stmt       {d} bits\n", .{@bitSizeOf(Stmt)});
// }

// test "Expr.init" {
//     var allocator = std.heap.page_allocator;
//     const ident = Expr.init(E.Identifier, E.Identifier{}, logger.Loc{ .start = 100 });
//     var list = [_]Expr{ident};
//     var expr = Expr.init(
//         E.Array,
//         E.Array{ .items = list[0..] },
//         logger.Loc{ .start = 1 },
//     );
//     try std.testing.expect(expr.loc.start == 1);
//     try std.testing.expect(@as(Expr.Tag, expr.data) == Expr.Tag.e_array);
//     try std.testing.expect(expr.data.e_array.items[0].loc.start == 100);

//     printmem("--Ref                      {d} bits\n", .{@bitSizeOf(Ref)});
//     printmem("--LocRef                   {d} bits\n", .{@bitSizeOf(LocRef)});
//     printmem("--logger.Loc               {d} bits\n", .{@bitSizeOf(logger.Loc)});
//     printmem("--logger.Range             {d} bits\n", .{@bitSizeOf(logger.Range)});
//     printmem("----------Expr:            {d} bits\n", .{@bitSizeOf(Expr)});
//     printmem("ExprNodeList:              {d} bits\n", .{@bitSizeOf(ExprNodeList)});
//     printmem("E.Array:                   {d} bits\n", .{@bitSizeOf(E.Array)});

//     printmem("E.Unary:                   {d} bits\n", .{@bitSizeOf(E.Unary)});
//     printmem("E.Binary:                  {d} bits\n", .{@bitSizeOf(E.Binary)});
//     printmem("E.Boolean:                 {d} bits\n", .{@bitSizeOf(E.Boolean)});
//     printmem("E.Super:                   {d} bits\n", .{@bitSizeOf(E.Super)});
//     printmem("E.Null:                    {d} bits\n", .{@bitSizeOf(E.Null)});
//     printmem("E.Undefined:               {d} bits\n", .{@bitSizeOf(E.Undefined)});
//     printmem("E.New:                     {d} bits\n", .{@bitSizeOf(E.New)});
//     printmem("E.NewTarget:               {d} bits\n", .{@bitSizeOf(E.NewTarget)});
//     printmem("E.Function:                {d} bits\n", .{@bitSizeOf(E.Function)});
//     printmem("E.ImportMeta:              {d} bits\n", .{@bitSizeOf(E.ImportMeta)});
//     printmem("E.Call:                    {d} bits\n", .{@bitSizeOf(E.Call)});
//     printmem("E.Dot:                     {d} bits\n", .{@bitSizeOf(E.Dot)});
//     printmem("E.Index:                   {d} bits\n", .{@bitSizeOf(E.Index)});
//     printmem("E.Arrow:                   {d} bits\n", .{@bitSizeOf(E.Arrow)});
//     printmem("E.Identifier:              {d} bits\n", .{@bitSizeOf(E.Identifier)});
//     printmem("E.ImportIdentifier:        {d} bits\n", .{@bitSizeOf(E.ImportIdentifier)});
//     printmem("E.PrivateIdentifier:       {d} bits\n", .{@bitSizeOf(E.PrivateIdentifier)});
//     printmem("E.JSXElement:              {d} bits\n", .{@bitSizeOf(E.JSXElement)});
//     printmem("E.Missing:                 {d} bits\n", .{@bitSizeOf(E.Missing)});
//     printmem("E.Number:                  {d} bits\n", .{@bitSizeOf(E.Number)});
//     printmem("E.BigInt:                  {d} bits\n", .{@bitSizeOf(E.BigInt)});
//     printmem("E.Object:                  {d} bits\n", .{@bitSizeOf(E.Object)});
//     printmem("E.Spread:                  {d} bits\n", .{@bitSizeOf(E.Spread)});
//     printmem("E.String:                  {d} bits\n", .{@bitSizeOf(E.String)});
//     printmem("E.TemplatePart:            {d} bits\n", .{@bitSizeOf(E.TemplatePart)});
//     printmem("E.Template:                {d} bits\n", .{@bitSizeOf(E.Template)});
//     printmem("E.RegExp:                  {d} bits\n", .{@bitSizeOf(E.RegExp)});
//     printmem("E.Await:                   {d} bits\n", .{@bitSizeOf(E.Await)});
//     printmem("E.Yield:                   {d} bits\n", .{@bitSizeOf(E.Yield)});
//     printmem("E.If:                      {d} bits\n", .{@bitSizeOf(E.If)});
//     printmem("E.RequireResolveString: {d} bits\n", .{@bitSizeOf(E.RequireResolveString)});
//     printmem("E.Import:                  {d} bits\n", .{@bitSizeOf(E.Import)});
//     printmem("----------Expr:            {d} bits\n", .{@bitSizeOf(Expr)});
// }

// -- ESBuild bit sizes
// EArray             | 256
// EArrow             | 512
// EAwait             | 192
// EBinary            | 448
// ECall              | 448
// EDot               | 384
// EIdentifier        | 96
// EIf                | 576
// EImport            | 448
// EImportIdentifier  | 96
// EIndex             | 448
// EJSXElement        | 448
// ENew               | 448
// EnumValue          | 384
// EObject            | 256
// EPrivateIdentifier | 64
// ERequire           | 32
// ERequireResolve    | 32
// EString            | 256
// ETemplate          | 640
// EUnary             | 256
// Expr               | 192
// ExprOrStmt         | 128
// EYield             | 128
// Finally            | 256
// Fn                 | 704
// FnBody             | 256
// LocRef             | 96
// NamedExport        | 96
// NamedImport        | 512
// NameMinifier       | 256
// NamespaceAlias     | 192
// opTableEntry       | 256
// Part               | 1088
// Property           | 640
// PropertyBinding    | 512
// Ref                | 64
// SBlock             | 192
// SBreak             | 64
// SClass             | 704
// SComment           | 128
// SContinue          | 64
// Scope              | 704
// ScopeMember        | 96
// SDirective         | 256
// SDoWhile           | 384
// SEnum              | 448
// SExportClause      | 256
// SExportDefault     | 256
// SExportEquals      | 192
// SExportFrom        | 320
// SExportStar        | 192
// SExpr              | 256
// SFor               | 384
// SForIn             | 576
// SForOf             | 640
// SFunction          | 768
// SIf                | 448
// SImport            | 320
// SLabel             | 320
// SLazyExport        | 192
// SLocal             | 256
// SNamespace         | 448
// Span               | 192
// SReturn            | 64
// SSwitch            | 448
// SThrow             | 192
// Stmt               | 192
// STry               | 384
// -- ESBuild bit sizes

const ToJSError = error{
    @"Cannot convert argument type to JS",
    @"Cannot convert identifier to JS. Try a statically-known value",
    MacroError,
    OutOfMemory,
};

const writeAnyToHasher = bun.writeAnyToHasher;

/// Say you need to allocate a bunch of tiny arrays
/// You could just do separate allocations for each, but that is slow
/// With std.ArrayList, pointers invalidate on resize and that means it will crash.
/// So a better idea is to batch up your allocations into one larger allocation
/// and then just make all the arrays point to different parts of the larger allocation
pub fn NewBatcher(comptime Type: type) type {
    return struct {
        head: []Type,

        pub fn init(allocator: std.mem.Allocator, count: usize) !@This() {
            const all = try allocator.alloc(Type, count);
            return @This(){ .head = all };
        }

        pub fn done(this: *@This()) void {
            bun.assert(this.head.len == 0); // count to init() was too large, overallocation
        }

        pub fn eat(this: *@This(), value: Type) *Type {
            return @as(*Type, @ptrCast(&this.head.eat1(value).ptr));
        }

        pub fn eat1(this: *@This(), value: Type) []Type {
            var prev = this.head[0..1];
            prev[0] = value;
            this.head = this.head[1..];
            return prev;
        }

        pub fn next(this: *@This(), values: anytype) []Type {
            this.head[0..values.len].* = values;
            const prev = this.head[0..values.len];
            this.head = this.head[values.len..];
            return prev;
        }
    };
}
