diff options
Diffstat (limited to 'src/renamer.zig')
-rw-r--r-- | src/renamer.zig | 520 |
1 files changed, 509 insertions, 11 deletions
diff --git a/src/renamer.zig b/src/renamer.zig index bce2d3030..88dcb769c 100644 --- a/src/renamer.zig +++ b/src/renamer.zig @@ -12,19 +12,19 @@ const C = bun.C; const std = @import("std"); const Ref = @import("./ast/base.zig").Ref; const logger = @import("bun").logger; +const JSLexer = @import("./js_lexer.zig"); -// This is...poorly named -// It does not rename -// It merely names -pub const Renamer = struct { +pub const NoOpRenamer = struct { symbols: js_ast.Symbol.Map, source: *const logger.Source, - pub fn init(symbols: js_ast.Symbol.Map, source: *const logger.Source) Renamer { - return Renamer{ .symbols = symbols, .source = source }; + pub fn init(symbols: js_ast.Symbol.Map, source: *const logger.Source) NoOpRenamer { + return NoOpRenamer{ .symbols = symbols, .source = source }; } - pub fn nameForSymbol(renamer: *Renamer, ref: Ref) string { + pub const originalName = nameForSymbol; + + pub fn nameForSymbol(renamer: *NoOpRenamer, ref: Ref) string { if (ref.isSourceContentsSlice()) { return renamer.source.contents[ref.sourceIndex() .. ref.sourceIndex() + ref.innerIndex()]; } @@ -37,11 +37,509 @@ pub const Renamer = struct { Global.panic("Invalid symbol {s} in {s}", .{ ref, renamer.source.path.text }); } } + + pub fn toRenamer(this: *NoOpRenamer) Renamer { + return .{ + .NoOpRenamer = this, + }; + } +}; + +pub const Renamer = union(enum) { + NumberRenamer: *NumberRenamer, + NoOpRenamer: *NoOpRenamer, + + pub fn symbols(this: Renamer) js_ast.Symbol.Map { + return switch (this) { + inline else => |r| r.symbols, + }; + } + + pub fn nameForSymbol(renamer: Renamer, ref: Ref) string { + return switch (renamer) { + inline else => |r| r.nameForSymbol(ref), + }; + } + + pub fn originalName(renamer: Renamer, ref: Ref) ?string { + return switch (renamer) { + inline else => |r| r.originalName(ref), + }; + } + + pub fn deinit(renamer: Renamer) void { + switch (renamer) { + .NumberRenamer => |r| r.deinit(), + else => {}, + } + } +}; + +pub const NumberRenamer = struct { + symbols: js_ast.Symbol.Map, + names: []bun.BabyList(string) = &.{}, + allocator: std.mem.Allocator, + temp_allocator: std.mem.Allocator, + number_scope_pool: bun.HiveArray(NumberScope, 128).Fallback, + arena: std.heap.ArenaAllocator, + root: NumberScope = .{}, + name_stack_fallback: std.heap.StackFallbackAllocator(512) = undefined, + name_temp_allocator: std.mem.Allocator = undefined, + + pub fn deinit(self: *NumberRenamer) void { + self.allocator.free(self.names); + self.root.deinit(self.temp_allocator); + self.arena.deinit(); + } + + pub fn toRenamer(this: *NumberRenamer) Renamer { + return .{ + .NumberRenamer = this, + }; + } + + pub fn originalName(r: *NumberRenamer, ref: Ref) string { + if (ref.isSourceContentsSlice()) { + unreachable; + } + + const resolved = r.symbols.follow(ref); + return r.symbols.getConst(resolved).?.original_name; + } + + pub fn assignName(r: *NumberRenamer, scope: *NumberScope, input_ref: Ref) void { + const ref = r.symbols.follow(input_ref); + + // Don't rename the same symbol more than once + var inner: *bun.BabyList(string) = &r.names[ref.sourceIndex()]; + if (inner.len > ref.innerIndex() and inner.at(ref.innerIndex()).len > 0) return; + + // Don't rename unbound symbols, symbols marked as reserved names, labels, or private names + const symbol = r.symbols.get(ref).?; + if (symbol.slotNamespace() != .default) { + return; + } + + r.name_stack_fallback.fixed_buffer_allocator.end_index = 0; + switch (scope.findUnusedName(r.allocator, r.name_temp_allocator, symbol.original_name)) { + .renamed => |name| { + const new_len = @max(inner.len, ref.innerIndex() + 1); + if (inner.cap <= new_len) { + const prev_cap = inner.len; + inner.ensureUnusedCapacity(r.allocator, new_len - prev_cap) catch unreachable; + const to_write = inner.ptr[prev_cap..inner.cap]; + @memset(std.mem.sliceAsBytes(to_write).ptr, 0, std.mem.sliceAsBytes(to_write).len); + } + inner.len = new_len; + inner.mut(ref.innerIndex()).* = name; + }, + .no_collision => {}, + } + } + + pub fn init( + allocator: std.mem.Allocator, + temp_allocator: std.mem.Allocator, + symbols: js_ast.Symbol.Map, + root_names: bun.StringHashMapUnmanaged(u32), + ) !*NumberRenamer { + var renamer = try allocator.create(NumberRenamer); + renamer.* = NumberRenamer{ + .symbols = symbols, + .allocator = allocator, + .temp_allocator = temp_allocator, + .names = try allocator.alloc(bun.BabyList(string), symbols.symbols_for_source.len), + .number_scope_pool = undefined, + .arena = std.heap.ArenaAllocator.init(temp_allocator), + }; + renamer.name_stack_fallback = .{ + .buffer = undefined, + .fallback_allocator = renamer.arena.allocator(), + .fixed_buffer_allocator = undefined, + }; + renamer.name_temp_allocator = renamer.name_stack_fallback.get(); + renamer.number_scope_pool = bun.HiveArray(NumberScope, 128).Fallback.init(renamer.arena.allocator()); + renamer.root.name_counts = root_names; + if (comptime Environment.allow_assert) { + if (std.os.getenv("BUN_DUMP_SYMBOLS") != null) + symbols.dump(); + } + + @memset(std.mem.sliceAsBytes(renamer.names).ptr, 0, std.mem.sliceAsBytes(renamer.names).len); + + return renamer; + } + + pub fn assignNamesRecursive(r: *NumberRenamer, scope: *js_ast.Scope, source_index: u32, parent: ?*NumberScope, sorted: *std.ArrayList(u32)) void { + var s = r.number_scope_pool.get(); + s.* = NumberScope{ + .parent = parent, + .name_counts = .{}, + }; + defer { + s.deinit(r.temp_allocator); + r.number_scope_pool.put(s); + } + + assignNamesRecursiveWithNumberScope(r, s, scope, source_index, sorted); + } + + fn assignNamesInScope( + r: *NumberRenamer, + s: *NumberScope, + scope: *js_ast.Scope, + source_index: u32, + sorted: *std.ArrayList(u32), + ) void { + { + sorted.clearRetainingCapacity(); + sorted.ensureUnusedCapacity(scope.members.count()) catch unreachable; + sorted.items.len = scope.members.count(); + var remaining = sorted.items; + var value_iter = scope.members.valueIterator(); + while (value_iter.next()) |value_ref| { + if (comptime Environment.allow_assert) + std.debug.assert(!value_ref.ref.isSourceContentsSlice()); + + remaining[0] = value_ref.ref.innerIndex(); + remaining = remaining[1..]; + } + std.debug.assert(remaining.len == 0); + std.sort.sort(u32, sorted.items, void{}, std.sort.asc(u32)); + + for (sorted.items) |inner_index| { + r.assignName(s, Ref.init(@intCast(Ref.Int, inner_index), source_index, false)); + } + } + + for (scope.generated.slice()) |ref| { + r.assignName(s, ref); + } + } + + pub fn assignNamesRecursiveWithNumberScope(r: *NumberRenamer, initial_scope: *NumberScope, scope_: *js_ast.Scope, source_index: u32, sorted: *std.ArrayList(u32)) void { + var s = initial_scope; + var scope = scope_; + defer if (s != initial_scope) { + s.deinit(r.temp_allocator); + r.number_scope_pool.put(s); + }; + + // Ignore function argument scopes + if (scope.kind == .function_args and scope.children.len == 1) { + scope = scope.children.ptr[0]; + std.debug.assert(scope.kind == .function_body); + } + + while (true) { + if (scope.members.count() > 0 or scope.generated.len > 0) { + var new_child_scope = r.number_scope_pool.get(); + new_child_scope.* = .{ + .parent = s, + .name_counts = .{}, + }; + s = new_child_scope; + + r.assignNamesInScope(s, scope, source_index, sorted); + } + + if (scope.children.len == 1) { + scope = scope.children.ptr[0]; + if (scope.kind == .function_args and scope.children.len == 1) { + scope = scope.children.ptr[0]; + std.debug.assert(scope.kind == .function_body); + } + } else { + break; + } + } + + // Symbols in child scopes may also have to be renamed to avoid conflicts + for (scope.children.slice()) |child| { + r.assignNamesRecursiveWithNumberScope(s, child, source_index, sorted); + } + } + + pub fn addTopLevelSymbol(r: *NumberRenamer, ref: Ref) void { + r.assignName(&r.root, ref); + } + + pub fn addTopLevelDeclaredSymbols(r: *NumberRenamer, declared_symbols: js_ast.DeclaredSymbol.List) void { + var decls = declared_symbols; + js_ast.DeclaredSymbol.forEachTopLevelSymbol(&decls, r, addTopLevelSymbol); + } + + pub fn nameForSymbol(renamer: *NumberRenamer, ref: Ref) string { + if (ref.isSourceContentsSlice()) { + bun.unreachablePanic("Unexpected unbound symbol!\n{any}", .{ref}); + } + + const resolved = renamer.symbols.follow(ref); + + const source_index = resolved.sourceIndex(); + const inner_index = resolved.innerIndex(); + + const renamed_list = renamer.names[source_index]; + + if (renamed_list.len > inner_index) { + const renamed = renamed_list.at(inner_index).*; + if (renamed.len > 0) { + return renamed; + } + } + + return renamer.symbols.symbols_for_source.at(source_index).at(inner_index).original_name; + } + + pub const NumberScope = struct { + parent: ?*NumberScope = null, + name_counts: bun.StringHashMapUnmanaged(u32) = .{}, + + pub fn deinit(this: *NumberScope, allocator: std.mem.Allocator) void { + this.name_counts.deinit(allocator); + this.* = undefined; + } + + pub const NameUse = union(enum) { + unused: void, + same_scope: u32, + used: void, + + pub fn find(this: *NumberScope, name: []const u8) NameUse { + // This version doesn't allocate + if (comptime Environment.allow_assert) + std.debug.assert(JSLexer.isIdentifier(name)); + + // avoid rehashing the same string over for each scope + const ctx = bun.StringHashMapContext.pre(name); + + if (this.name_counts.getAdapted(name, ctx)) |count| { + return .{ .same_scope = count }; + } + + var s: ?*NumberScope = this.parent; + + while (s) |scope| : (s = scope.parent) { + if (scope.name_counts.containsAdapted(name, ctx)) { + return .{ .used = void{} }; + } + } + + return .{ .unused = void{} }; + } + }; + + const UnusedName = union(enum) { + no_collision: void, + renamed: string, + }; + + /// Caller must use an arena allocator + pub fn findUnusedName(this: *NumberScope, allocator: std.mem.Allocator, temp_allocator: std.mem.Allocator, input_name: []const u8) UnusedName { + var name = bun.MutableString.ensureValidIdentifier(input_name, temp_allocator) catch unreachable; + + switch (NameUse.find(this, name)) { + .unused => {}, + else => |use| { + var tries: u32 = if (use == .used) + 1 + else + // To avoid O(n^2) behavior, the number must start off being the number + // that we used last time there was a collision with this name. Otherwise + // if there are many collisions with the same name, each name collision + // would have to increment the counter past all previous name collisions + // which is a O(n^2) time algorithm. Only do this if this symbol comes + // from the same scope as the previous one since sibling scopes can reuse + // the same name without problems. + use.same_scope; + + const prefix = name; + + tries += 1; + + var mutable_name = MutableString.initEmpty(temp_allocator); + mutable_name.growIfNeeded(prefix.len + 4) catch unreachable; + mutable_name.appendSlice(prefix) catch unreachable; + mutable_name.appendInt(tries) catch unreachable; + + switch (NameUse.find(this, mutable_name.toOwnedSliceLeaky())) { + .unused => { + name = mutable_name.toOwnedSliceLeaky(); + + if (use == .same_scope) { + var existing = this.name_counts.getOrPut(allocator, prefix) catch unreachable; + if (!existing.found_existing) { + if (strings.eqlLong(input_name, prefix, true)) { + existing.key_ptr.* = input_name; + } else { + existing.key_ptr.* = allocator.dupe(u8, prefix) catch unreachable; + } + } + + existing.value_ptr.* = tries; + } + }, + else => |cur_use| { + while (true) { + mutable_name.resetTo(prefix.len); + mutable_name.appendInt(tries) catch unreachable; + + tries += 1; + + switch (NameUse.find(this, mutable_name.toOwnedSliceLeaky())) { + .unused => { + if (cur_use == .same_scope) { + var existing = this.name_counts.getOrPut(allocator, prefix) catch unreachable; + if (!existing.found_existing) { + if (strings.eqlLong(input_name, prefix, true)) { + existing.key_ptr.* = input_name; + } else { + existing.key_ptr.* = allocator.dupe(u8, prefix) catch unreachable; + } + } + + existing.value_ptr.* = tries; + } + + name = mutable_name.toOwnedSliceLeaky(); + break; + }, + else => {}, + } + } + }, + } + }, + } + + // Each name starts off with a count of 1 so that the first collision with + // "name" is called "name2" + if (strings.eqlLong(name, input_name, true)) { + this.name_counts.putNoClobber(allocator, input_name, 1) catch unreachable; + return .{ .no_collision = {} }; + } + + name = allocator.dupe(u8, name) catch unreachable; + + this.name_counts.putNoClobber(allocator, name, 1) catch unreachable; + return .{ .renamed = name }; + } + }; }; -pub const DisabledRenamer = struct { - pub fn init(_: js_ast.Symbol.Map) DisabledRenamer {} - pub inline fn nameForSymbol(_: *Renamer, _: js_ast.Ref) string { - @compileError("DisabledRunner called"); +pub const ExportRenamer = struct { + string_buffer: bun.MutableString, + used: bun.StringHashMap(u32), + + pub fn init(allocator: std.mem.Allocator) ExportRenamer { + return ExportRenamer{ + .string_buffer = MutableString.initEmpty(allocator), + .used = bun.StringHashMap(u32).init(allocator), + }; + } + + pub fn clearRetainingCapacity(this: *ExportRenamer) void { + this.used.clearRetainingCapacity(); + this.string_buffer.reset(); + } + + pub fn deinit(this: *ExportRenamer) void { + this.used.deinit(); + this.string_buffer.deinit(); + } + + pub fn nextRenamedName(this: *ExportRenamer, input: []const u8) string { + var entry = this.used.getOrPut(input) catch unreachable; + var tries: u32 = 1; + if (entry.found_existing) { + while (true) { + this.string_buffer.reset(); + var writer = this.string_buffer.writer(); + writer.print("{s}{d}", .{ input, tries }) catch unreachable; + tries += 1; + var attempt = this.string_buffer.toOwnedSliceLeaky(); + entry = this.used.getOrPut(attempt) catch unreachable; + if (!entry.found_existing) { + const to_use = this.string_buffer.allocator.dupe(u8, attempt) catch unreachable; + entry.key_ptr.* = to_use; + entry.value_ptr.* = tries; + + entry = this.used.getOrPut(input) catch unreachable; + entry.value_ptr.* = tries; + return to_use; + } + } + } else { + entry.value_ptr.* = tries; + } + + return entry.key_ptr.*; } }; + +pub fn computeInitialReservedNames( + allocator: std.mem.Allocator, +) !bun.StringHashMapUnmanaged(u32) { + var names = bun.StringHashMapUnmanaged(u32){}; + + const extras = .{ + "Promise", + "Require", + }; + + try names.ensureTotalCapacityContext( + allocator, + @truncate(u32, JSLexer.Keywords.keys().len + JSLexer.StrictModeReservedWords.keys().len + 1 + extras.len), + bun.StringHashMapContext{}, + ); + + for (JSLexer.Keywords.keys()) |keyword| { + names.putAssumeCapacity(keyword, 1); + } + + for (JSLexer.StrictModeReservedWords.keys()) |keyword| { + names.putAssumeCapacity(keyword, 1); + } + + inline for (comptime extras) |extra| { + names.putAssumeCapacity(extra, 1); + } + + return names; +} + +pub fn computeReservedNamesForScope( + scope: *js_ast.Scope, + symbols: *const js_ast.Symbol.Map, + names_: *bun.StringHashMapUnmanaged(u32), + allocator: std.mem.Allocator, +) void { + var names = names_.*; + defer names_.* = names; + + var member_iter = scope.members.valueIterator(); + while (member_iter.next()) |member| { + const symbol = symbols.get(member.ref).?; + if (symbol.kind == .unbound or symbol.must_not_be_renamed) { + names.put(allocator, symbol.original_name, 1) catch unreachable; + } + } + + for (scope.generated.slice()) |ref| { + const symbol = symbols.get(ref).?; + if (symbol.kind == .unbound or symbol.must_not_be_renamed) { + names.put(allocator, symbol.original_name, 1) catch unreachable; + } + } + + // If there's a direct "eval" somewhere inside the current scope, continue + // traversing down the scope tree until we find it to get all reserved names + if (scope.contains_direct_eval) { + for (scope.children.slice()) |child| { + if (child.contains_direct_eval) { + names_.* = names; + computeReservedNamesForScope(child, symbols, &names, allocator); + } + } + } +} |