diff options
author | 2021-05-16 23:25:12 -0700 | |
---|---|---|
committer | 2021-05-16 23:25:12 -0700 | |
commit | 154e049638753abc10ed0eca2012685fe3b831be (patch) | |
tree | bdeb6b0bf8137ee36df0aab436ac50713ddeb5ef | |
parent | e80f865974df7aae5e2f6abb966b36497da693c6 (diff) | |
download | bun-154e049638753abc10ed0eca2012685fe3b831be.tar.gz bun-154e049638753abc10ed0eca2012685fe3b831be.tar.zst bun-154e049638753abc10ed0eca2012685fe3b831be.zip |
lots
Former-commit-id: 9ccb4dd082afbc4f94982bf092360487232d8b60
-rw-r--r-- | .vscode/launch.json | 58 | ||||
-rw-r--r-- | src/allocators.zig | 304 | ||||
-rw-r--r-- | src/ast/base.zig | 2 | ||||
-rw-r--r-- | src/bundler.zig | 1 | ||||
-rw-r--r-- | src/cache.zig | 31 | ||||
-rw-r--r-- | src/fs.zig | 157 | ||||
-rw-r--r-- | src/js_lexer.zig | 9 | ||||
-rw-r--r-- | src/js_parser/js_parser.zig | 220 | ||||
-rw-r--r-- | src/js_printer.zig | 29 | ||||
-rw-r--r-- | src/json_parser.zig | 12 | ||||
-rw-r--r-- | src/resolver/package_json.zig | 7 | ||||
-rw-r--r-- | src/resolver/resolve_path.zig | 521 | ||||
-rw-r--r-- | src/resolver/resolver.zig | 401 | ||||
-rw-r--r-- | src/test/fixtures/exports-bug.js | 2342 | ||||
-rw-r--r-- | src/test/fixtures/label-continue-break-bug.js | 125 | ||||
-rw-r--r-- | src/test/fixtures/symbols-bug.js | 16 | ||||
-rw-r--r-- | src/test/tester.zig | 4 |
17 files changed, 3739 insertions, 500 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json index ac77daf98..ac18a6ebb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,14 +21,30 @@ // "--resolve=disable", // "--cwd", // "${workspaceFolder}", - // "src/test/fixtures/cannot-assign-to-import-bug.js", + // "/Users/jarredsumner/Code/esdev/src/test/fixtures/exports-bug.js", + // "-o", + // "out" + // ], + // "cwd": "${workspaceFolder}", + // "console": "internalConsole" + // } + // { + // "type": "lldb", + // "request": "launch", + // "name": "Dev Launch", + // "program": "${workspaceFolder}/build/debug/macos-x86_64/esdev", + // "preLaunchTask": "build", + // "args": [ + // "--resolve=disable", + // "--cwd", + // "/Users/jarredsumner/Code/esdev/src/test/fixtures/", + // "/Users/jarredsumner/Code/esdev/src/test/fixtures/symbols-bug.js", // "-o", // "out" // ], // "cwd": "${workspaceFolder}", // "console": "internalConsole" // } - { "type": "lldb", "request": "launch", @@ -38,8 +54,8 @@ "args": [ "--resolve=dev", "--cwd", - "./src/api/demo", - "pages/index.js", + "/Users/jarredsumner/Builds/esbuild/bench/three/src/", + "./entry.js", "-o", "out" ], @@ -50,6 +66,40 @@ // "type": "lldb", // "request": "launch", // "name": "Dev Launch", + // "program": "${workspaceFolder}/build/debug/macos-x86_64/esdev", + // "preLaunchTask": "build", + // "args": [ + // "--resolve=dev", + // "--cwd", + // "/Users/jarredsumner/Builds/esbuild/bench/three/src/", + // "./entry.js", + // "-o", + // "out" + // ], + // "cwd": "${workspaceFolder}", + // "console": "internalConsole" + // } + // { + // "type": "lldb", + // "request": "launch", + // "name": "Dev Launch", + // "program": "${workspaceFolder}/build/debug/macos-x86_64/esdev", + // "preLaunchTask": "build", + // "args": [ + // "--resolve=dev", + // "--cwd", + // "./src/api/demo", + // "pages/index.js", + // "-o", + // "out" + // ], + // "cwd": "${workspaceFolder}", + // "console": "internalConsole" + // } + // { + // "type": "lldb", + // "request": "launch", + // "name": "Dev Launch", // "program": "${workspaceFolder}/build/bin/debug/esdev", // "preLaunchTask": "build", // "args": [ diff --git a/src/allocators.zig b/src/allocators.zig new file mode 100644 index 000000000..b6a13ba47 --- /dev/null +++ b/src/allocators.zig @@ -0,0 +1,304 @@ +const std = @import("std"); + +const Wyhash = std.hash.Wyhash; +const FixedBufferAllocator = std.heap.FixedBufferAllocator; + +// https://en.wikipedia.org/wiki/.bss#BSS_in_C +pub fn BSSSectionAllocator(comptime size: usize) type { + return struct { + var backing_buf: [size]u8 = undefined; + var fixed_buffer_allocator = FixedBufferAllocator.init(&backing_buf); + var buf_allocator = &fixed_buffer_allocator.allocator; + const Allocator = std.mem.Allocator; + const Self = @This(); + + allocator: Allocator, + fallback_allocator: *Allocator, + + is_overflowed: bool = false, + + pub fn get(self: *Self) *Allocator { + return &self.allocator; + } + + pub fn init(fallback_allocator: *Allocator) Self { + return Self{ .fallback_allocator = fallback_allocator, .allocator = Allocator{ + .allocFn = BSSSectionAllocator(size).alloc, + .resizeFn = BSSSectionAllocator(size).resize, + } }; + } + + pub fn alloc( + allocator: *Allocator, + len: usize, + ptr_align: u29, + len_align: u29, + return_address: usize, + ) error{OutOfMemory}![]u8 { + const self = @fieldParentPtr(Self, "allocator", allocator); + return buf_allocator.allocFn(buf_allocator, len, ptr_align, len_align, return_address) catch |err| { + self.is_overflowed = true; + return self.fallback_allocator.allocFn(self.fallback_allocator, len, ptr_align, len_align, return_address); + }; + } + + pub fn resize( + allocator: *Allocator, + buf: []u8, + buf_align: u29, + new_len: usize, + len_align: u29, + return_address: usize, + ) error{OutOfMemory}!usize { + const self = @fieldParentPtr(Self, "allocator", allocator); + if (fixed_buffer_allocator.ownsPtr(buf.ptr)) { + return fixed_buffer_allocator.allocator.resizeFn(&fixed_buffer_allocator.allocator, buf, buf_align, new_len, len_align, return_address); + } else { + return self.fallback_allocator.resizeFn(self.fallback_allocator, buf, buf_align, new_len, len_align, return_address); + } + } + }; +} + +const HashKeyType = u64; +const IndexMap = std.HashMapUnmanaged(HashKeyType, u32, hash_hashFn, hash_eqlFn, 80); +pub const Result = struct { + hash: HashKeyType, + index: u32, + status: ItemStatus, + + pub fn hasCheckedIfExists(r: *Result) bool { + return r.status != .unknown; + } +}; +const Seed = 999; +pub const NotFound = std.math.maxInt(u32); +pub const Unassigned = NotFound - 1; + +pub fn hash_hashFn(key: HashKeyType) HashKeyType { + return key; +} + +pub fn hash_eqlFn(a: HashKeyType, b: HashKeyType) bool { + return a == b; +} + +pub const ItemStatus = packed enum(u3) { + unknown, + exists, + not_found, +}; + +const hasDeinit = std.meta.trait.hasFn("deinit")(ValueType); + +pub fn BSSMap(comptime ValueType: type, comptime count: anytype, store_keys: bool, estimated_key_length: usize) type { + const max_index = count - 1; + const BSSMapType = struct { + pub var backing_buf: [count]ValueType = undefined; + pub var backing_buf_used: u16 = 0; + const Allocator = std.mem.Allocator; + const Self = @This(); + + // const HashTableAllocator = BSSSectionAllocator(@bitSizeOf(HashKeyType) * count * 2); + + index: IndexMap, + overflow_list: std.ArrayListUnmanaged(ValueType), + allocator: *Allocator, + + pub var instance: Self = undefined; + + pub fn init(allocator: *std.mem.Allocator) *Self { + instance = Self{ + .index = IndexMap{}, + .allocator = allocator, + .overflow_list = std.ArrayListUnmanaged(ValueType){}, + }; + + return &instance; + } + + pub fn isOverflowing() bool { + return backing_buf_used >= @as(u16, count); + } + + pub fn getOrPut(self: *Self, key: []const u8) !Result { + const _key = Wyhash.hash(Seed, key); + var index = try self.index.getOrPut(self.allocator, _key); + + if (index.found_existing) { + return Result{ + .hash = _key, + .index = index.entry.value, + .status = switch (index.entry.value) { + NotFound => .not_found, + Unassigned => .unknown, + else => .exists, + }, + }; + } + index.entry.value = Unassigned; + + return Result{ + .hash = _key, + .index = Unassigned, + .status = .unknown, + }; + } + + pub fn get(self: *const Self, key: []const u8) ?*ValueType { + const _key = Wyhash.hash(Seed, key); + const index = self.index.get(_key) orelse return null; + return self.atIndex(index); + } + + pub fn markNotFound(self: *Self, result: Result) void { + self.index.put(self.allocator, result.hash, NotFound) catch unreachable; + } + + pub fn atIndex(self: *const Self, index: u32) ?*ValueType { + return switch (index) { + NotFound, Unassigned => null, + 0...max_index => &backing_buf[index], + else => &self.overflow_list.items[index - count], + }; + } + + pub fn put(self: *Self, result: *Result, value: ValueType) !*ValueType { + var index: u32 = @intCast(u32, backing_buf_used + 1); + if (index >= max_index) { + const real_index = self.overflow_list.items.len; + index += @truncate(u32, real_index); + try self.overflow_list.append(self.allocator, value); + result.index = index; + self.index.putAssumeCapacity(result.hash, index); + return &self.overflow_list.items[real_index]; + } else { + backing_buf_used += 1; + backing_buf[index] = value; + result.index = index; + self.index.putAssumeCapacity(result.hash, index); + if (backing_buf_used >= max_index - 1) { + self.overflow_list = try @TypeOf(self.overflow_list).initCapacity(self.allocator, count); + } + return &backing_buf[index]; + } + } + + pub fn remove(self: *Self, key: string) u32 { + const _key = Wyhash.hash(Seed, key); + const index = self.index.get(_key) orelse return; + switch (index) { + Unassigned => { + self.index.remove(_key); + }, + NotFound => { + self.index.remove(_key); + }, + 0...max_index => { + if (hasDeinit(ValueType)) { + backing_buf[index].deinit(); + } + backing_buf[index] = undefined; + }, + else => { + const i = index - count; + if (hasDeinit(ValueType)) { + self.overflow_list.items[i].deinit(); + } + self.overflow_list.items[index - count] = undefined; + }, + } + + return index; + } + }; + if (!store_keys) { + return BSSMapType; + } + + return struct { + map: *BSSMapType, + const Self = @This(); + pub var instance: Self = undefined; + var key_list_buffer: [count * estimated_key_length]u8 = undefined; + var key_list_buffer_used: usize = 0; + var key_list_slices: [count][]u8 = undefined; + var key_list_overflow: std.ArrayListUnmanaged([]u8) = undefined; + + pub fn init(allocator: *std.mem.Allocator) *Self { + instance = Self{ + .map = BSSMapType.init(allocator), + }; + + return &instance; + } + + pub fn isOverflowing() bool { + return instance.map.backing_buf_used >= count; + } + pub fn getOrPut(self: *Self, key: []const u8) !Result { + return try self.map.getOrPut(key); + } + pub fn get(self: *Self, key: []const u8) ?*ValueType { + return @call(.{ .modifier = .always_inline }, BSSMapType.get, .{ self.map, key }); + } + + pub fn atIndex(self: *Self, index: u32) ?*ValueType { + return @call(.{ .modifier = .always_inline }, BSSMapType.atIndex, .{ self.map, index }); + } + + pub fn keyAtIndex(self: *Self, index: u32) ?[]const u8 { + return switch (index) { + Unassigned, NotFound => null, + 0...max_index => { + return key_list_slices[index]; + }, + else => { + return key_list_overflow.items[index - count]; + }, + }; + } + + pub fn put(self: *Self, key: anytype, comptime store_key: bool, result: *Result, value: ValueType) !*ValueType { + var ptr = try self.map.put(result, value); + if (store_key) { + try self.putKey(key, result); + } + + return ptr; + } + + pub fn putKey(self: *Self, key: anytype, result: *Result) !void { + if (key_list_buffer_used + key.len < key_list_buffer.len) { + const start = key_list_buffer_used; + key_list_buffer_used += key.len; + var slice = key_list_buffer[start..key_list_buffer_used]; + std.mem.copy(u8, slice, key); + + if (result.index < count) { + key_list_slices[result.index] = slice; + } else { + try key_list_overflow.append(self.map.allocator, slice); + } + } else if (result.index > key_list_overflow.items.len) { + try key_list_overflow.append(self.map.allocator, try self.map.allocator.dupe(u8, key)); + } else { + const real_index = result.index - count; + if (key_list_overflow.items[real_index].len > 0) { + self.map.allocator.free(key_list_overflow.items[real_index]); + } + + key_list_overflow.items[real_index] = try self.map.allocator.dupe(u8, key); + } + } + + pub fn markNotFound(self: *Self, result: Result) void { + self.map.markNotFound(result); + } + + // For now, don't free the keys. + pub fn remove(self: *Self, key: string) u32 { + return self.map.remove(key); + } + }; +} diff --git a/src/ast/base.zig b/src/ast/base.zig index 904ad97bd..8e2635004 100644 --- a/src/ast/base.zig +++ b/src/ast/base.zig @@ -34,7 +34,7 @@ pub const Ref = packed struct { .source_index = std.math.maxInt(Ref.Int), }; pub fn toInt(int: anytype) Int { - return std.math.cast(Ref.Int, int) catch 0; + return std.math.lossyCast(Ref.Int, int); } pub fn isNull(self: *const Ref) bool { return self.source_index == std.math.maxInt(Ref.Int) and self.inner_index == std.math.maxInt(Ref.Int); diff --git a/src/bundler.zig b/src/bundler.zig index 34983a9b8..a59120840 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -265,7 +265,6 @@ pub const Bundler = struct { try msg.writeFormat(std.io.getStdOut().writer()); } } - switch (bundler.options.resolve_mode) { .lazy, .dev, .bundle => { while (bundler.resolve_queue.readItem()) |item| { diff --git a/src/cache.zig b/src/cache.zig index 88fa6e9ea..22953a3a2 100644 --- a/src/cache.zig +++ b/src/cache.zig @@ -19,7 +19,7 @@ pub const Cache = struct { pub fn init(allocator: *std.mem.Allocator) Set { return Set{ - .js = JavaScript{}, + .js = JavaScript.init(allocator), .fs = Fs{ .mutex = Mutex.init(), .entries = std.StringHashMap(Fs.Entry).init(allocator), @@ -130,13 +130,14 @@ pub const Cache = struct { }; pub const JavaScript = struct { - pub const Entry = struct { - ast: js_ast.Ast, - source: logger.Source, - ok: bool, - msgs: []logger.Msg, - }; + mutex: Mutex, + entries: std.StringHashMap(Result), + pub const Result = js_ast.Result; + + pub fn init(allocator: *std.mem.Allocator) JavaScript { + return JavaScript{ .mutex = Mutex.init(), .entries = std.StringHashMap(Result).init(allocator) }; + } // For now, we're not going to cache JavaScript ASTs. // It's probably only relevant when bundling for production. pub fn parse( @@ -147,19 +148,31 @@ pub const Cache = struct { log: *logger.Log, source: *const logger.Source, ) anyerror!?js_ast.Ast { + cache.mutex.lock(); + defer cache.mutex.unlock(); + + var get_or_put_result = try cache.entries.getOrPut(source.key_path.text); + + if (get_or_put_result.found_existing) { + return if (get_or_put_result.entry.value.ok) get_or_put_result.entry.value.ast else null; + } + var temp_log = logger.Log.init(allocator); var parser = js_parser.Parser.init(opts, &temp_log, source, defines, allocator) catch |err| { temp_log.appendTo(log) catch {}; + get_or_put_result.entry.value = Result{ .ast = undefined, .ok = false }; + return null; }; - const result = parser.parse() catch |err| { + get_or_put_result.entry.value = parser.parse() catch |err| { + get_or_put_result.entry.value = Result{ .ast = undefined, .ok = false }; temp_log.appendTo(log) catch {}; return null; }; temp_log.appendTo(log) catch {}; - return if (result.ok) result.ast else null; + return if (get_or_put_result.entry.value.ok) get_or_put_result.entry.value.ast else null; } }; diff --git a/src/fs.zig b/src/fs.zig index 86a6550c4..99998d446 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -6,11 +6,13 @@ const expect = std.testing.expect; const Mutex = sync.Mutex; const Semaphore = sync.Semaphore; -const resolvePath = @import("./resolver/resolve_path.zig").resolvePath; +const path_handler = @import("./resolver/resolve_path.zig"); + +const allocators = @import("./allocators.zig"); // pub const FilesystemImplementation = @import("fs_impl.zig"); -threadlocal var scratch_lookup_buffer = [_]u8{0} ** 255; +threadlocal var scratch_lookup_buffer: [256]u8 = undefined; pub const FileSystem = struct { allocator: *std.mem.Allocator, @@ -41,6 +43,14 @@ pub const FileSystem = struct { dir: string, data: EntryMap, + pub fn updateDir(i: *DirEntry, dir: string) void { + var iter = i.data.iterator(); + i.dir = dir; + while (iter.next()) |entry| { + entry.value.dir = dir; + } + } + pub fn empty(dir: string, allocator: *std.mem.Allocator) DirEntry { return DirEntry{ .dir = dir, .data = EntryMap.init(allocator) }; } @@ -154,10 +164,26 @@ pub const FileSystem = struct { // pub fn readDir(fs: *FileSystemEntry, path: string) ?[]string { // } + pub fn normalize(f: *@This(), str: string) string { + return @call(.{ .modifier = .always_inline }, path_handler.normalizeAndJoin, .{ f.top_level_dir, .auto, str }); + } + + pub fn join(f: *@This(), parts: anytype) string { + return @call(.{ .modifier = .always_inline }, path_handler.normalizeAndJoinString, .{ + f.top_level_dir, + parts, + .auto, + }); + } + + pub fn joinAlloc(f: *@This(), allocator: *std.mem.Allocator, parts: anytype) !string { + const joined = f.join(parts); + return try allocator.dupe(u8, joined); + } pub const RealFS = struct { entries_mutex: Mutex = Mutex.init(), - entries: std.StringHashMap(EntriesOption), + entries: *EntriesOption.Map, allocator: *std.mem.Allocator, do_not_cache_entries: bool = false, limiter: Limiter, @@ -166,7 +192,7 @@ pub const FileSystem = struct { pub fn init(allocator: *std.mem.Allocator, enable_watcher: bool) RealFS { return RealFS{ - .entries = std.StringHashMap(EntriesOption).init(allocator), + .entries = EntriesOption.Map.init(allocator), .allocator = allocator, .limiter = Limiter.init(allocator), .watcher = if (enable_watcher) std.StringHashMap(WatchData).init(allocator) else null, @@ -276,6 +302,11 @@ pub const FileSystem = struct { entries, err, }; + + // This custom map implementation: + // - Preallocates a fixed amount of directory name space + // - Doesn't store directory names which don't exist. + pub const Map = allocators.BSSMap(EntriesOption, 1024, true, 128); }; // Limit the number of files open simultaneously to avoid ulimit issues @@ -305,15 +336,20 @@ pub const FileSystem = struct { } }; - fn readdir(fs: *RealFS, _dir: string) !DirEntry { + pub fn openDir(fs: *RealFS, unsafe_dir_string: string) std.fs.File.OpenError!std.fs.Dir { + return try std.fs.openDirAbsolute(unsafe_dir_string, std.fs.Dir.OpenDirOptions{ .iterate = true, .access_sub_paths = true }); + } + + fn readdir( + fs: *RealFS, + _dir: string, + handle: std.fs.Dir, + ) !DirEntry { fs.limiter.before(); defer fs.limiter.after(); - var handle = try std.fs.openDirAbsolute(_dir, std.fs.Dir.OpenDirOptions{ .iterate = true, .access_sub_paths = true }); - defer handle.close(); - var iter: std.fs.Dir.Iterator = handle.iterate(); - var dir = DirEntry{ .data = DirEntry.EntryMap.init(fs.allocator), .dir = _dir }; + var dir = DirEntry.init("", fs.allocator); errdefer dir.deinit(); while (try iter.next()) |_entry| { const entry: std.fs.Dir.Entry = _entry; @@ -342,7 +378,7 @@ pub const FileSystem = struct { var entry_ptr = try fs.allocator.create(Entry); entry_ptr.* = Entry{ .base = name, - .dir = _dir, + .dir = "", .mutex = Mutex.init(), // Call "stat" lazily for performance. The "@material-ui/icons" package // contains a directory with over 11,000 entries in it and running "stat" @@ -356,12 +392,11 @@ pub const FileSystem = struct { try dir.data.put(name, entry_ptr); } - // Copy at the bottom here so in the event of an error, we don't deinit the dir string. - dir.dir = _dir; + return dir; } - fn readDirectoryError(fs: *RealFS, dir: string, err: anyerror) !void { + fn readDirectoryError(fs: *RealFS, dir: string, err: anyerror) !*EntriesOption { if (fs.watcher) |*watcher| { fs.watcher_mutex.lock(); defer fs.watcher_mutex.unlock(); @@ -371,27 +406,52 @@ pub const FileSystem = struct { if (!fs.do_not_cache_entries) { fs.entries_mutex.lock(); defer fs.entries_mutex.unlock(); - - try fs.entries.put(dir, EntriesOption{ + var get_or_put_result = try fs.entries.getOrPut(dir); + var opt = try fs.entries.put(null, false, &get_or_put_result, EntriesOption{ .err = DirEntry.Err{ .original_err = err, .canonical_error = err }, }); + + return opt; } + + temp_entries_option = EntriesOption{ + .err = DirEntry.Err{ .original_err = err, .canonical_error = err }, + }; + return &temp_entries_option; } - pub fn readDirectory(fs: *RealFS, dir: string) !EntriesOption { + + threadlocal var temp_entries_option: EntriesOption = undefined; + + pub fn readDirectory(fs: *RealFS, dir: string, _handle: ?std.fs.Dir, recursive: bool) !*EntriesOption { + var cache_result: ?allocators.Result = null; + if (!fs.do_not_cache_entries) { fs.entries_mutex.lock(); defer fs.entries_mutex.unlock(); - // First, check the cache - if (fs.entries.get(dir)) |_dir| { - return _dir; + cache_result = try fs.entries.getOrPut(dir); + + if (cache_result.?.hasCheckedIfExists()) { + if (fs.entries.atIndex(cache_result.?.index)) |cached_result| { + return cached_result; + } + } + } + + var handle = _handle orelse try fs.openDir(dir); + + defer { + if (_handle == null) { + handle.close(); } } // Cache miss: read the directory entries - const entries = fs.readdir(dir) catch |err| { - _ = fs.readDirectoryError(dir, err) catch {}; - return err; + const entries = fs.readdir( + dir, + handle, + ) catch |err| { + return fs.readDirectoryError(dir, err) catch unreachable; }; if (fs.watcher) |*watcher| { @@ -409,16 +469,23 @@ pub const FileSystem = struct { WatchData{ .dir_entries = names, .state = .dir_has_entries }, ); } - - fs.entries_mutex.lock(); - defer fs.entries_mutex.unlock(); - const result = EntriesOption{ - .entries = entries, - }; if (!fs.do_not_cache_entries) { - try fs.entries.put(dir, result); + fs.entries_mutex.lock(); + defer fs.entries_mutex.unlock(); + const result = EntriesOption{ + .entries = entries, + }; + + var entries_ptr = try fs.entries.put(dir, true, &cache_result.?, result); + const dir_key = fs.entries.keyAtIndex(cache_result.?.index) orelse unreachable; + entries_ptr.entries.updateDir(dir_key); + return entries_ptr; } - return result; + + temp_entries_option = EntriesOption{ .entries = entries }; + temp_entries_option.entries.updateDir(try fs.allocator.dupe(u8, dir)); + + return &temp_entries_option; } fn readFileError(fs: *RealFS, path: string, err: anyerror) void { @@ -622,6 +689,7 @@ pub const PathName = struct { }; threadlocal var normalize_buf: [1024]u8 = undefined; +threadlocal var join_buf: [1024]u8 = undefined; pub const Path = struct { pretty: string, @@ -634,35 +702,6 @@ pub const Path = struct { return try std.fmt.allocPrint(allocator, "{s}://{s}", .{ p.namespace, p.text }); } - // for now, assume you won't try to normalize a path longer than 1024 chars - pub fn normalize(str: string, allocator: *std.mem.Allocator) string { - if (str.len == 0 or (str.len == 1 and str[0] == ' ')) return "."; - if (resolvePath(&normalize_buf, str)) |out| { - return allocator.dupe(u8, out) catch unreachable; - } - return str; - } - - // for now, assume you won't try to normalize a path longer than 1024 chars - pub fn normalizeNoAlloc(str: string, comptime remap_windows_paths: bool) string { - if (str.len == 0 or (str.len == 1 and (str[0] == ' ' or str[0] == '\\'))) return "."; - - if (remap_windows_paths) { - std.mem.copy(u8, &normalize_buf, str); - var i: usize = 0; - while (i < str.len) : (i += 1) { - if (str[i] == '\\') { - normalize_buf[i] = '/'; - } - } - } - - if (resolvePath(&normalize_buf, str)) |out| { - return out; - } - return str; - } - pub fn init(text: string) Path { return Path{ .pretty = text, .text = text, .namespace = "file", .name = PathName.init(text) }; } diff --git a/src/js_lexer.zig b/src/js_lexer.zig index 89e2e06dd..96b6f6835 100644 --- a/src/js_lexer.zig +++ b/src/js_lexer.zig @@ -275,7 +275,6 @@ pub const Lexer = struct { } // Reset string literal - lexer.string_literal = &([_]u16{}); lexer.string_literal_slice = lexer.source.contents[lexer.start + 1 .. lexer.end - suffixLen]; lexer.string_literal_is_ascii = !needs_slow_path; lexer.string_literal_buffer.shrinkRetainingCapacity(0); @@ -283,8 +282,6 @@ pub const Lexer = struct { lexer.string_literal_buffer.ensureTotalCapacity(lexer.string_literal_slice.len) catch unreachable; var slice = lexer.string_literal_buffer.allocatedSlice(); lexer.string_literal_buffer.items = slice[0..strings.toUTF16Buf(lexer.string_literal_slice, slice)]; - lexer.string_literal = lexer.string_literal_buffer.items; - lexer.string_literal_slice = &[_]u8{}; } if (quote == '\'' and lexer.json_options != null) { @@ -483,6 +480,7 @@ pub const Lexer = struct { if (lexer.code_point == '\\') { try lexer.scanIdentifierWithEscapes(); lexer.token = T.t_private_identifier; + // lexer.Identifier, lexer.Token = lexer.scanIdentifierWithEscapes(normalIdentifier); } else { lexer.token = T.t_private_identifier; @@ -766,7 +764,6 @@ pub const Lexer = struct { lexer.token = .t_slash_equals; }, '/' => { - try lexer.step(); singleLineComment: while (true) { try lexer.step(); switch (lexer.code_point) { @@ -1440,14 +1437,12 @@ pub const Lexer = struct { lexer.token = .t_string_literal; lexer.string_literal_slice = lexer.source.contents[lexer.start + 1 .. lexer.end - 1]; - lexer.string_literal.len = lexer.string_literal_slice.len; lexer.string_literal_is_ascii = !needs_decode; - lexer.string_literal_buffer.shrinkRetainingCapacity(0); + lexer.string_literal_buffer.clearRetainingCapacity(); if (needs_decode) { lexer.string_literal_buffer.ensureTotalCapacity(lexer.string_literal_slice.len) catch unreachable; try lexer.decodeJSXEntities(lexer.string_literal_slice, &lexer.string_literal_buffer); lexer.string_literal = lexer.string_literal_buffer.items; - lexer.string_literal_slice = &([_]u8{0}); } } diff --git a/src/js_parser/js_parser.zig b/src/js_parser/js_parser.zig index 39860b42e..641fa312d 100644 --- a/src/js_parser/js_parser.zig +++ b/src/js_parser/js_parser.zig @@ -533,25 +533,27 @@ pub const SideEffects = enum { equality.ok = equality.equal; }, .e_undefined => |l| { - equality.equal = @as(Expr.Tag, right) == Expr.Tag.e_undefined; - equality.ok = equality.equal; + equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_undefined; + equality.equal = equality.ok; }, .e_boolean => |l| { equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_boolean; - equality.equal = l.value == right.e_boolean.value; + equality.equal = equality.ok and l.value == right.e_boolean.value; }, .e_number => |l| { equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_number; - equality.equal = l.value == right.e_number.value; + equality.equal = equality.ok and l.value == right.e_number.value; }, .e_big_int => |l| { equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_big_int; - equality.equal = strings.eql(l.value, right.e_big_int.value); + equality.equal = equality.ok and strings.eql(l.value, right.e_big_int.value); }, .e_string => |l| { equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_string; - const r = right.e_string; - equality.equal = r.eql(E.String, l); + if (equality.ok) { + const r = right.e_string; + equality.equal = r.eql(E.String, l); + } }, else => {}, } @@ -790,7 +792,7 @@ pub const SideEffects = enum { return Result{ .ok = true, .value = !strings.eqlComptime(e.value, "0"), .side_effects = .no_side_effects }; }, .e_string => |e| { - return Result{ .ok = true, .value = e.value.len > 0, .side_effects = .no_side_effects }; + return Result{ .ok = true, .value = std.math.max(e.value.len, e.utf8.len) > 0, .side_effects = .no_side_effects }; }, .e_function, .e_arrow, .e_reg_exp => { return Result{ .ok = true, .value = true, .side_effects = .no_side_effects }; @@ -1790,7 +1792,7 @@ pub const P = struct { } const str = arg.data.e_string; - const import_record_index = p.addImportRecord(.dynamic, arg.loc, p.lexer.utf16ToString(str.value)); + const import_record_index = p.addImportRecord(.dynamic, arg.loc, str.string(p.allocator) catch unreachable); p.import_records.items[import_record_index].handles_import_errors = (state.is_await_target and p.fn_or_arrow_data_visit.try_body_count != 0) or state.is_then_catch_target; p.import_records_for_current_part.append(import_record_index) catch unreachable; return p.e(E.Import{ @@ -1892,13 +1894,14 @@ pub const P = struct { } pub fn findSymbol(p: *P, loc: logger.Loc, name: string) !FindSymbolResult { - var ref: Ref = Ref{}; + var ref: Ref = undefined; var declare_loc: logger.Loc = undefined; var is_inside_with_scope = false; var did_forbid_argumen = false; - var scope = p.current_scope; + var _scope: ?*Scope = p.current_scope; + var did_match = false; - while (true) { + while (_scope) |scope| : (_scope = _scope.?.parent) { // Track if we're inside a "with" statement body if (scope.kind == .with) { @@ -1916,19 +1919,17 @@ pub const P = struct { if (scope.members.get(name)) |member| { ref = member.ref; declare_loc = member.loc; + did_match = true; break; } + } - if (scope.parent) |parent| { - scope = parent; - } else { - // Allocate an "unbound" symbol - p.checkForNonBMPCodePoint(loc, name); - ref = try p.newSymbol(.unbound, name); - declare_loc = loc; - try p.module_scope.members.put(name, js_ast.Scope.Member{ .ref = ref, .loc = logger.Loc.Empty }); - break; - } + if (!did_match) { + // Allocate an "unbound" symbol + p.checkForNonBMPCodePoint(loc, name); + ref = p.newSymbol(.unbound, name) catch unreachable; + declare_loc = loc; + p.module_scope.members.put(name, js_ast.Scope.Member{ .ref = ref, .loc = logger.Loc.Empty }) catch unreachable; } // If we had to pass through a "with" statement body to get to the symbol @@ -1997,9 +1998,12 @@ pub const P = struct { // code regions since those will be culled. if (!p.is_control_flow_dead) { p.symbols.items[ref.inner_index].use_count_estimate += 1; - var use = p.symbol_uses.get(ref) orelse Symbol.Use{}; - use.count_estimate += 1; - p.symbol_uses.put(ref, use) catch unreachable; + var result = p.symbol_uses.getOrPut(ref) catch unreachable; + if (!result.found_existing) { + result.entry.value = Symbol.Use{ .count_estimate = 1 }; + } else { + result.entry.value.count_estimate += 1; + } } // The correctness of TypeScript-to-JavaScript conversion relies on accurate @@ -2283,8 +2287,59 @@ pub const P = struct { if (!symbol.isHoisted()) { continue :nextMember; } + + // Check for collisions that would prevent to hoisting "var" symbols up to the enclosing function scope + var __scope = scope.parent; + + while (__scope) |_scope| { + // Variable declarations hoisted past a "with" statement may actually end + // up overwriting a property on the target of the "with" statement instead + // of initializing the variable. We must not rename them or we risk + // causing a behavior change. + // + // var obj = { foo: 1 } + // with (obj) { var foo = 2 } + // assert(foo === undefined) + // assert(obj.foo === 2) + // + if (_scope.kind == .with) { + symbol.must_not_be_renamed = true; + } + + if (_scope.members.getEntry(symbol.original_name)) |existing_member_entry| { + const existing_member = existing_member_entry.value; + const existing_symbol: Symbol = p.symbols.items[existing_member.ref.inner_index]; + + // We can hoist the symbol from the child scope into the symbol in + // this scope if: + // + // - The symbol is unbound (i.e. a global variable access) + // - The symbol is also another hoisted variable + // - The symbol is a function of any kind and we're in a function or module scope + // + // Is this unbound (i.e. a global access) or also hoisted? + if (existing_symbol.kind == .unbound or existing_symbol.kind == .hoisted or + (Symbol.isKindFunction(existing_symbol.kind) and (_scope.kind == .entry or _scope.kind == .function_body))) + { + // Silently merge this symbol into the existing symbol + symbol.link = existing_member.ref; + _scope.members.put(symbol.original_name, existing_member) catch unreachable; + continue :nextMember; + } + } + + if (_scope.kindStopsHoisting()) { + _scope.members.put(symbol.original_name, res.value) catch unreachable; + break; + } + __scope = _scope.parent; + } } } + + for (scope.children.items) |_item, i| { + p.hoistSymbols(scope.children.items[i]); + } } pub fn nextScopeInOrderForVisitPass(p: *P) ScopeOrder { @@ -2737,7 +2792,7 @@ pub const P = struct { } var parseStmtOpts = ParseStatementOptions{}; - p.declareBinding(.hoisted, arg, &parseStmtOpts) catch unreachable; + p.declareBinding(.hoisted, &arg, &parseStmtOpts) catch unreachable; var default_value: ?ExprNodeIndex = null; if (!func.flags.has_rest_arg and p.lexer.token == .t_equals) { @@ -3523,7 +3578,7 @@ pub const P = struct { // jarred: TIL! if (p.lexer.token != .t_open_brace) { try p.lexer.expect(.t_open_paren); - const value = try p.parseBinding(); + var value = try p.parseBinding(); // Skip over types if (p.options.ts and p.lexer.token == .t_colon) { @@ -3542,7 +3597,7 @@ pub const P = struct { else => {}, } stmtOpts = ParseStatementOptions{}; - try p.declareBinding(kind, value, &stmtOpts); + try p.declareBinding(kind, &value, &stmtOpts); binding = value; } @@ -4533,6 +4588,8 @@ pub const P = struct { const name = p.lexer.identifier; const loc = p.lexer.loc(); + const e_str = p.lexer.toEString(); + if (!p.lexer.isIdentifierOrKeyword()) { try p.lexer.expect(.t_identifier); } @@ -4541,7 +4598,7 @@ pub const P = struct { const ref = p.storeNameInRef(name) catch unreachable; - key = p.e(p.lexer.toEString(), loc); + key = p.e(e_str, loc); if (p.lexer.token != .t_colon and p.lexer.token != .t_open_paren) { const value = p.b(B.Identifier{ .ref = ref }, loc); @@ -4590,7 +4647,7 @@ pub const P = struct { var value: ?js_ast.Expr = null; var local = try p.parseBinding(); - p.declareBinding(kind, local, opts) catch unreachable; + p.declareBinding(kind, &local, opts) catch unreachable; // Skip over types if (p.options.ts) { @@ -5097,9 +5154,9 @@ pub const P = struct { try p.lexer.expect(T.t_equals_greater_than); - for (args) |arg| { + for (args) |*arg| { var opts = ParseStatementOptions{}; - try p.declareBinding(Symbol.Kind.hoisted, arg.binding, &opts); + try p.declareBinding(Symbol.Kind.hoisted, &arg.binding, &opts); } // The ability to call "super()" is inherited by arrow functions @@ -5125,7 +5182,7 @@ pub const P = struct { return E.Arrow{ .args = args, .prefer_expr = true, .body = G.FnBody{ .loc = arrow_loc, .stmts = stmts } }; } - pub fn declareBinding(p: *P, kind: Symbol.Kind, binding: BindingNodeIndex, opts: *ParseStatementOptions) !void { + pub fn declareBinding(p: *P, kind: Symbol.Kind, binding: *BindingNodeIndex, opts: *ParseStatementOptions) !void { switch (binding.data) { .b_missing => {}, .b_identifier => |bind| { @@ -5135,15 +5192,14 @@ pub const P = struct { }, .b_array => |bind| { - for (bind.items) |item| { - p.declareBinding(kind, item.binding, opts) catch unreachable; + for (bind.items) |item, i| { + p.declareBinding(kind, &bind.items[i].binding, opts) catch unreachable; } }, .b_object => |bind| { for (bind.properties) |*prop| { - const value = prop.value; - p.declareBinding(kind, value, opts) catch unreachable; + p.declareBinding(kind, &prop.value, opts) catch unreachable; } }, @@ -5571,7 +5627,7 @@ pub const P = struct { } } - key = p.e(p.lexer.toEString(), name_range.loc); + key = p.e(E.String{ .utf8 = name }, name_range.loc); // Parse a shorthand property if (!opts.is_class and kind == .normal and p.lexer.token != .t_colon and p.lexer.token != .t_open_paren and p.lexer.token != .t_less_than and !opts.is_generator and !js_lexer.Keywords.has(name)) { @@ -5691,7 +5747,7 @@ pub const P = struct { if (opts.is_class and !is_computed) { switch (key.data) { .e_string => |str| { - if (!opts.is_static and strings.eqlUtf16("constructor", str.value)) { + if (!opts.is_static and str.eql(string, "constructor")) { if (kind == .get) { p.log.addRangeError(p.source, key_range, "Class constructor cannot be a getter") catch unreachable; } else if (kind == .set) { @@ -5703,7 +5759,7 @@ pub const P = struct { } else { is_constructor = true; } - } else if (opts.is_static and strings.eqlUtf16("prototype", str.value)) { + } else if (opts.is_static and str.eql(string, "prototype")) { p.log.addRangeError(p.source, key_range, "Invalid static method name \"prototype\"") catch unreachable; } }, @@ -5894,7 +5950,7 @@ pub const P = struct { if (opts.ts_decorators.len > 0) { switch ((property.key orelse p.panic("Internal error: Expected property {s} to have a key.", .{property})).data) { .e_string => |str| { - if (strings.eqlUtf16("constructor", str.value)) { + if (str.eql(string, "constructor")) { p.log.addError(p.source, first_decorator_loc, "TypeScript does not allow decorators on class constructors") catch unreachable; } }, @@ -8586,7 +8642,7 @@ pub const P = struct { in.assign_target, is_delete_target, e_.target, - if (e_.index.data.e_string.isUTF8()) p.lexer.utf16ToString(e_.index.data.e_string.value) else e_.index.data.e_string.utf8, + e_.index.data.e_string.string(p.allocator) catch unreachable, e_.index.loc, is_call_target, )) |val| { @@ -8598,7 +8654,7 @@ pub const P = struct { // though this is a run-time error, we make it a compile-time error when // bundling because scope hoisting means these will no longer be run-time // errors. - if ((in.assign_target != .none or is_delete_target) and @as(Expr.Tag, e_.target.data) == .e_identifier) { + if ((in.assign_target != .none or is_delete_target) and @as(Expr.Tag, e_.target.data) == .e_identifier and p.symbols.items[e_.target.data.e_identifier.ref.inner_index].kind == .import) { const r = js_lexer.rangeOfIdentifier(p.source, e_.target.loc); p.log.addRangeErrorFmt( p.source, @@ -8818,14 +8874,10 @@ pub const P = struct { var has_spread = false; var has_proto = false; - var i: usize = 0; - while (i < e_.properties.len) : (i += 1) { - var property = e_.properties[i]; - + for (e_.properties) |*property, i| { if (property.kind != .spread) { - const key = p.visitExpr(property.key orelse Global.panic("Expected property key", .{})); - e_.properties[i].key = key; - + property.key = p.visitExpr(property.key orelse Global.panic("Expected property key", .{})); + const key = property.key.?; // Forbid duplicate "__proto__" properties according to the specification if (!property.flags.is_computed and !property.flags.was_shorthand and !property.flags.is_method and in.assign_target == .none and key.data.isStringValue() and strings.eqlComptime( // __proto__ is utf8, assume it lives in refs @@ -8873,9 +8925,6 @@ pub const P = struct { } } } - - // TODO: can we avoid htis copy - e_.properties[i] = property; } }, .e_import => |e_| { @@ -9208,7 +9257,7 @@ pub const P = struct { for (ex.properties) |property| { // The key must still be evaluated if it's computed or a spread - if (property.kind == .spread or property.flags.is_computed) { + if (property.kind == .spread or property.flags.is_computed or property.flags.is_spread) { return false; } @@ -9554,9 +9603,12 @@ pub const P = struct { if (data.label) |*label| { const name = p.loadNameFromRef(label.ref orelse p.panic("Expected label to have a ref", .{})); const res = p.findLabelSymbol(label.loc, name); - - label.ref = res.ref; - } else if (p.fn_or_arrow_data_visit.is_inside_loop and !p.fn_or_arrow_data_visit.is_inside_switch) { + if (res.found) { + label.ref = res.ref; + } else { + data.label = null; + } + } else if (!p.fn_or_arrow_data_visit.is_inside_loop and !p.fn_or_arrow_data_visit.is_inside_switch) { const r = js_lexer.rangeOfIdentifier(p.source, stmt.loc); p.log.addRangeError(p.source, r, "Cannot use \"break\" here") catch unreachable; } @@ -10289,9 +10341,7 @@ pub const P = struct { } }, .b_object => |bind| { - var i: usize = 0; - while (i < bind.properties.len) : (i += 1) { - var property = bind.properties[i]; + for (bind.properties) |*property| { if (!property.flags.is_spread) { property.key = p.visitExpr(property.key); } @@ -10312,7 +10362,6 @@ pub const P = struct { else => {}, } } - bind.properties[i] = property; } }, else => { @@ -10377,19 +10426,17 @@ pub const P = struct { var _scope: ?*Scope = p.current_scope; - while (_scope) |scope| : (_scope = scope.parent) { - var label_ref = scope.label_ref orelse continue; - - if (!scope.kindStopsHoisting() or (scope.kind != .label) or !strings.eql(name, p.symbols.items[label_ref.inner_index].original_name)) { - continue; + while (_scope != null and !_scope.?.kindStopsHoisting()) : (_scope = _scope.?.parent.?) { + const scope = _scope orelse unreachable; + const label_ref = scope.label_ref orelse continue; + if (scope.kind == .label and strings.eql(name, p.symbols.items[label_ref.inner_index].original_name)) { + // Track how many times we've referenced this symbol + p.recordUsage(label_ref); + res.ref = label_ref; + res.is_loop = scope.label_stmt_is_loop; + res.found = true; + return res; } - - // Track how many times we've referenced this symbol - p.recordUsage(label_ref); - res.ref = label_ref; - res.is_loop = scope.label_stmt_is_loop; - res.found = true; - break; } const r = js_lexer.rangeOfIdentifier(p.source, loc); @@ -10471,12 +10518,7 @@ pub const P = struct { if (is_private) {} else if (!property.flags.is_method and !property.flags.is_computed) { if (property.key) |key| { if (@as(Expr.Tag, key.data) == .e_string) { - const str = key.data.e_string; - if (str.isUTF8()) { - name_to_keep = p.lexer.utf16ToString(key.data.e_string.value); - } else { - name_to_keep = str.utf8; - } + name_to_keep = key.data.e_string.string(p.allocator) catch unreachable; } } } @@ -10869,8 +10911,8 @@ pub const P = struct { // with no statements while (i < parts.len) : (i += 1) { var part = parts[i]; - _ = p.import_records_for_current_part.toOwnedSlice(); - _ = p.declared_symbols.toOwnedSlice(); + p.import_records_for_current_part.shrinkRetainingCapacity(0); + p.declared_symbols.shrinkRetainingCapacity(0); var result = try ImportScanner.scan(p, part.stmts); kept_import_equals = kept_import_equals or result.kept_import_equals; @@ -10898,6 +10940,22 @@ pub const P = struct { } parts = parts[0..parts_end]; + // Do a second pass for exported items now that imported items are filled out + for (parts) |part| { + for (part.stmts) |stmt| { + switch (stmt.data) { + .s_export_clause => |clause| { + for (clause.items) |item| { + if (p.named_imports.getEntry(item.name.ref.?)) |_import| { + _import.value.is_exported = true; + } + } + }, + else => {}, + } + } + } + // Analyze cross-part dependencies for tree shaking and code splitting { diff --git a/src/js_printer.zig b/src/js_printer.zig index a0142983f..e72eefbde 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -1229,8 +1229,7 @@ pub fn NewPrinter(comptime ascii_only: bool) type { p.options.indent += 1; } - var i: usize = 0; - while (i < e.properties.len) : (i += 1) { + for (e.properties) |property, i| { if (i != 0) { p.print(","); if (e.is_single_line) { @@ -1242,7 +1241,7 @@ pub fn NewPrinter(comptime ascii_only: bool) type { p.printNewline(); p.printIndent(); } - p.printProperty(e.properties[i]); + p.printProperty(property); } if (!e.is_single_line) { @@ -1642,6 +1641,7 @@ pub fn NewPrinter(comptime ascii_only: bool) type { p.printExpr(item.value.?, .comma, ExprFlag.None()); return; } + const _key = item.key orelse unreachable; if (item.flags.is_static) { p.print("static"); @@ -1686,7 +1686,7 @@ pub fn NewPrinter(comptime ascii_only: bool) type { if (item.flags.is_computed) { p.print("["); - p.printExpr(item.key.?, .comma, ExprFlag.None()); + p.printExpr(_key, .comma, ExprFlag.None()); p.print("]"); if (item.value) |val| { @@ -1711,12 +1711,12 @@ pub fn NewPrinter(comptime ascii_only: bool) type { return; } - switch (item.key.?.data) { + switch (_key.data) { .e_private_identifier => |key| { p.printSymbol(key.ref); }, .e_string => |key| { - p.addSourceMapping(item.key.?.loc); + p.addSourceMapping(_key.loc); if (key.isUTF8()) { p.printSpaceBeforeIdentifier(); p.printIdentifier(key.utf8); @@ -1786,14 +1786,21 @@ pub fn NewPrinter(comptime ascii_only: bool) type { } } } else { - const c = p.bestQuoteCharForString(key.value, false); - p.print(c); - p.printQuotedUTF16(key.value, c); - p.print(c); + if (key.isUTF8()) { + const c = p.bestQuoteCharForString(key.utf8, false); + p.print(c); + p.printIdentifier(key.utf8); + p.print(c); + } else { + const c = p.bestQuoteCharForString(key.value, false); + p.print(c); + p.printQuotedUTF16(key.value, c); + p.print(c); + } } }, else => { - p.printExpr(item.key.?, .lowest, ExprFlag{}); + p.printExpr(_key, .lowest, ExprFlag{}); }, } diff --git a/src/json_parser.zig b/src/json_parser.zig index ee28c2a93..eb51c2e53 100644 --- a/src/json_parser.zig +++ b/src/json_parser.zig @@ -93,17 +93,7 @@ fn JSONLikeParser(opts: js_lexer.JSONOptions) type { return p.e(E.Null{}, loc); }, .t_string_literal => { - var str: E.String = undefined; - if (p.lexer.string_literal_is_ascii) { - str = E.String{ - .utf8 = p.lexer.string_literal_slice, - }; - } else { - const value = p.lexer.stringLiteralUTF16(); - str = E.String{ - .value = value, - }; - } + var str: E.String = p.lexer.toEString(); try p.lexer.next(); return p.e(str, loc); diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig index 68d6bacb1..ea62c81cf 100644 --- a/src/resolver/package_json.zig +++ b/src/resolver/package_json.zig @@ -50,7 +50,10 @@ pub const PackageJSON = struct { errdefer r.allocator.free(package_json_path); const entry = r.caches.fs.readFile(r.fs, input_path) catch |err| { - r.log.addErrorFmt(null, logger.Loc.Empty, r.allocator, "Cannot read file \"{s}\": {s}", .{ r.prettyPath(fs.Path.init(input_path)), @errorName(err) }) catch unreachable; + if (err != error.IsDir) { + r.log.addErrorFmt(null, logger.Loc.Empty, r.allocator, "Cannot read file \"{s}\": {s}", .{ r.prettyPath(fs.Path.init(input_path)), @errorName(err) }) catch unreachable; + } + return null; }; @@ -146,7 +149,7 @@ pub const PackageJSON = struct { // import of "foo", but that's actually not a bug. Or arguably it's a // bug in Browserify but we have to replicate this bug because packages // do this in the wild. - const key = fs.Path.normalize(_key_str, r.allocator); + const key = r.allocator.dupe(u8, r.fs.normalize(_key_str)) catch unreachable; switch (value.data) { .e_string => |str| { diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index f639bff1b..9cb3e635c 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -1,83 +1,498 @@ -// https://github.com/MasterQ32/ftz/blob/3183b582211f8e38c1c3363c56753026ca45c11f/src/main.zig#L431-L509 -// Thanks, Felix! We should get this into std perhaps. +const tester = @import("../test/tester.zig"); const std = @import("std"); -/// Resolves a unix-like path and removes all "." and ".." from it. Will not escape the root and can be used to sanitize inputs. -pub fn resolvePath(buffer: []u8, src_path: []const u8) ?[]u8 { - var end: usize = 0; - buffer[0] = '.'; +threadlocal var parser_join_input_buffer: [1024]u8 = undefined; +threadlocal var parser_buffer: [1024]u8 = undefined; - var iter = std.mem.tokenize(src_path, "/"); - while (iter.next()) |segment| { - if (end >= buffer.len) break; +// This function is based on Node.js' path.normalize function. +// https://github.com/nodejs/node/blob/36bb31be5f0b85a0f6cbcb36b64feb3a12c60984/lib/path.js#L66 +pub fn normalizeStringGeneric(str: []const u8, buf: []u8, comptime allow_above_root: bool, comptime separator: u8, comptime isPathSeparator: anytype, lastIndexOfSeparator: anytype) []u8 { + var i: usize = 0; + var last_segment_length: i32 = 0; + var last_slash: i32 = -1; + var dots: i32 = 0; + var code: u8 = 0; - if (std.mem.eql(u8, segment, ".")) { - continue; - } else if (std.mem.eql(u8, segment, "..")) { - while (true) { - if (end == 0) - break; - if (buffer[end] == '/') { - break; + var written_len: usize = 0; + const stop_len = str.len; + + while (i <= stop_len) : (i += 1) { + if (i < stop_len) { + code = str[i]; + } else if (@call(std.builtin.CallOptions{ .modifier = .always_inline }, isPathSeparator, .{code})) { + break; + } else { + code = separator; + } + + if (@call(std.builtin.CallOptions{ .modifier = .always_inline }, isPathSeparator, .{code})) { + if (last_slash == @intCast(i32, i) - 1 or dots == 1) { + // NOOP + } else if (dots == 2) { + if (written_len < 2 or last_segment_length != 2 or buf[written_len - 1] != '.' or buf[written_len - 2] != '.') { + if (written_len > 2) { + if (lastIndexOfSeparator(buf[0..written_len])) |last_slash_index| { + written_len = last_slash_index; + last_segment_length = @intCast(i32, written_len - 1 - (lastIndexOfSeparator(buf[0..written_len]) orelse 0)); + } else { + written_len = 0; + } + last_slash = @intCast(i32, i); + dots = 0; + continue; + } else if (written_len != 0) { + written_len = 0; + last_segment_length = 0; + last_slash = @intCast(i32, i); + dots = 0; + continue; + } + + if (allow_above_root) { + if (written_len > 0) { + buf[written_len] = separator; + written_len += 1; + } + + buf[written_len] = '.'; + written_len += 1; + buf[written_len] = '.'; + written_len += 1; + + last_segment_length = 2; + } + } + } else { + if (written_len > 0) { + buf[written_len] = separator; + written_len += 1; } - end -= 1; + + const slice = str[@intCast(usize, @intCast(usize, last_slash + 1))..i]; + std.mem.copy(u8, buf[written_len .. written_len + slice.len], slice); + written_len += slice.len; + last_segment_length = @intCast(i32, i) - last_slash - 1; } + + last_slash = @intCast(i32, i); + dots = 0; + } else if (code == '.' and dots != -1) { + dots += 1; } else { - if (end + segment.len + 1 > buffer.len) + dots = -1; + } + } + + return buf[0..written_len]; +} + +pub const Platform = enum { + auto, + loose, + windows, + posix, + + pub fn isSeparator(comptime _platform: Platform, char: u8) bool { + const platform = _platform.resolve(); + switch (platform) { + .auto => unreachable, + .loose => { + return isSepAny(char); + }, + .windows => { + return isSepWin32(char); + }, + .posix => { + return isSepPosix(char); + }, + } + } + + pub fn leadingSeparatorIndex(comptime _platform: Platform, path: anytype) ?usize { + switch (_platform.resolve()) { + .windows => { + if (path.len < 1) + return null; + + if (path[0] == '/') + return 0; + + if (path[0] == '\\') + return 0; + + if (path.len < 3) + return null; + + // C:\ + // C:/ + if (path[0] >= 'A' and path[0] <= 'Z' and path[1] == ':') { + if (path[2] == '/') + return 2; + if (path[2] == '\\') + return 2; + } + return null; + }, + .posix => { + if (path.len > 0 and path[0] == '/') { + return 0; + } else { + return null; + } + }, + else => { + return leadingSeparatorIndex(.windows, path) orelse leadingSeparatorIndex(.posix, path); + }, + } + } + + pub fn resolve(comptime _platform: Platform) Platform { + if (_platform == .auto) { + switch (std.Target.current.os.tag) { + .windows => { + return .windows; + }, + + .freestanding, .emscripten, .other => { + return .loose; + }, - const start = end; - buffer[end] = '/'; - end += segment.len + 1; - std.mem.copy(u8, buffer[start + 1 .. end], segment); + else => { + return .posix; + }, + } } + + return _platform; + } +}; + +pub fn normalizeString(str: []const u8, comptime allow_above_root: bool, comptime _platform: Platform) []u8 { + return normalizeStringBuf(str, &parser_buffer, allow_above_root, _platform); +} + +pub fn normalizeStringBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool, comptime _platform: Platform) []u8 { + comptime const platform = _platform.resolve(); + + switch (platform) { + .auto => unreachable, + + .windows => { + return normalizeStringWindowsBuf(str, buf, allow_above_root); + }, + .posix => { + return normalizeStringPosixBuf(str, buf, allow_above_root); + }, + + .loose => { + return normalizeStringLooseBuf(str, buf, allow_above_root); + }, + } +} + +pub fn normalizeStringAlloc(allocator: *std.mem.Allocator, str: []const u8, comptime allow_above_root: bool, comptime _platform: Platform) ![]const u8 { + return try allocator.dupe(u8, normalizeString(str, allow_above_root, _platform)); +} + +pub fn normalizeAndJoin2(_cwd: []const u8, comptime _platform: Platform, part: anytype, part2: anytype) []const u8 { + const parts = [_][]const u8{ part, part2 }; + const slice = normalizeAndJoinString(_cwd, &parts, _platform); + return slice; +} + +pub fn normalizeAndJoin(_cwd: []const u8, comptime _platform: Platform, part: anytype) []const u8 { + const parts = [_][]const u8{ + part, + }; + const slice = normalizeAndJoinString(_cwd, &parts, _platform); + return slice; +} + +// Convert parts of potentially invalid file paths into a single valid filpeath +// without querying the filesystem +// This is the equivalent of +pub fn normalizeAndJoinString(_cwd: []const u8, parts: anytype, comptime _platform: Platform) []const u8 { + return normalizeAndJoinStringBuf(_cwd, &parser_join_input_buffer, parts, _platform); +} + +pub fn normalizeAndJoinStringBuf(_cwd: []const u8, buf: []u8, parts: anytype, comptime _platform: Platform) []const u8 { + if (parts.len == 0) { + return _cwd; + } + + if ((_platform == .loose or _platform == .posix) and parts.len == 1 and parts[0].len == 1 and parts[0] == std.fs.path.sep_posix) { + return "/"; + } + + var cwd = _cwd; + var out: usize = 0; + // When parts[0] is absolute, we treat that as, effectively, the cwd + var ignore_cwd = cwd.len == 0; + + // Windows leading separators can be a lot of things... + // So we need to do this instead of just checking the first char. + var leading_separator: []const u8 = ""; + if (_platform.leadingSeparatorIndex(parts[0])) |leading_separator_i| { + leading_separator = parts[0][0 .. leading_separator_i + 1]; + ignore_cwd = true; } - const result = if (end == 0) - buffer[0 .. end + 1] - else - buffer[0..end]; + if (!ignore_cwd) { + leading_separator = cwd[0 .. 1 + (_platform.leadingSeparatorIndex(_cwd) orelse unreachable)]; // cwd must be absolute + cwd = _cwd[leading_separator.len..cwd.len]; + out = cwd.len; + std.debug.assert(out < buf.len); + std.mem.copy(u8, buf[0..out], cwd); + } + + for (parts) |part, i| { + // This never returns leading separators. + var normalized_part = normalizeString(part, true, _platform); + if (normalized_part.len == 0) { + continue; + } + switch (_platform.resolve()) { + .windows => { + buf[out] = std.fs.path.sep_windows; + }, + else => { + buf[out] = std.fs.path.sep_posix; + }, + } + + out += 1; - if (std.mem.eql(u8, result, src_path)) { - return null; + const start = out; + out += normalized_part.len; + std.debug.assert(out < buf.len); + std.mem.copy(u8, buf[start..out], normalized_part); } - return result; + // One last normalization, to remove any ../ added + const result = normalizeStringBuf(buf[0..out], parser_buffer[leading_separator.len..parser_buffer.len], false, _platform); + std.mem.copy(u8, buf[0..leading_separator.len], leading_separator); + std.mem.copy(u8, buf[leading_separator.len .. result.len + leading_separator.len], result); + + return buf[0 .. result.len + leading_separator.len]; +} + +pub fn isSepPosix(char: u8) bool { + return char == std.fs.path.sep_posix; +} + +pub fn isSepWin32(char: u8) bool { + return char == std.fs.path.sep_windows; +} + +pub fn isSepAny(char: u8) bool { + return @call(.{ .modifier = .always_inline }, isSepPosix, .{char}) or @call(.{ .modifier = .always_inline }, isSepWin32, .{char}); } -fn testResolve(expected: []const u8, input: []const u8) !void { - var buffer: [1024]u8 = undefined; +pub fn lastIndexOfSeparatorWindows(slice: []const u8) ?usize { + return std.mem.lastIndexOfScalar(u8, slice, std.fs.path.sep_windows); +} - const actual = try resolvePath(&buffer, input); - std.testing.expectEqualStrings(expected, actual); +pub fn lastIndexOfSeparatorPosix(slice: []const u8) ?usize { + return std.mem.lastIndexOfScalar(u8, slice, std.fs.path.sep_posix); } -test "resolvePath" { - try testResolve("/", ""); - try testResolve("/", "/"); - try testResolve("/", "////////////"); +pub fn lastIndexOfSeparatorLoose(slice: []const u8) ?usize { + return std.mem.lastIndexOfAny(u8, slice, "/\\"); +} - try testResolve("/a", "a"); - try testResolve("/a", "/a"); - try testResolve("/a", "////////////a"); - try testResolve("/a", "////////////a///"); +pub fn normalizeStringPosix(str: []const u8, comptime allow_above_root: bool) []u8 { + return normalizeStringGenericBuf(str, &parser_buffer, allow_above_root, std.fs.path.sep_posix, isSepPosix, lastIndexOfSeparatorPosix); +} - try testResolve("/a/b/c/d", "/a/b/c/d"); +pub fn normalizeStringPosixBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool) []u8 { + return normalizeStringGeneric(str, buf, allow_above_root, std.fs.path.sep_posix, isSepPosix, lastIndexOfSeparatorPosix); +} - try testResolve("/a/b/d", "/a/b/c/../d"); +pub fn normalizeStringWindows(str: []const u8, comptime allow_above_root: bool) []u8 { + return normalizeStringGenericBuf(str, &parser_buffer, allow_above_root, std.fs.path.sep_windows, isSepWin32, lastIndexOfSeparatorWindows); +} + +pub fn normalizeStringWindowsBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool) []u8 { + return normalizeStringGeneric(str, buf, allow_above_root, std.fs.path.sep_windows, isSepWin32, lastIndexOfSeparatorWindows); +} + +pub fn normalizeStringLoose(str: []const u8, comptime allow_above_root: bool) []u8 { + return normalizeStringGenericBuf(str, &parser_buffer, allow_above_root, std.fs.path.sep_posix, isSepAny, lastIndexOfSeparatorLoose); +} + +pub fn normalizeStringLooseBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool) []u8 { + return normalizeStringGeneric(str, buf, allow_above_root, std.fs.path.sep_posix, isSepAny, lastIndexOfSeparatorLoose); +} + +test "normalizeAndJoinStringPosix" { + var t = tester.Tester.t(std.heap.c_allocator); + defer t.report(@src()); + const string = []const u8; + const cwd = "/Users/jarredsumner/Code/app"; + + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/bar/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "file.js" }, .posix), + @src(), + ); + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "../file.js" }, .posix), + @src(), + ); + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "./bar", "../file.js" }, .posix), + @src(), + ); + + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "././././foo", "././././bar././././", "../file.js" }, .posix), + @src(), + ); + _ = t.expect( + "/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "././././bar././././", "../file.js" }, .posix), + @src(), + ); + + _ = t.expect( + "/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", ".", "././././bar././././", ".", "../file.js" }, .posix), + @src(), + ); + + _ = t.expect( + "/Code/app/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "..", "././././bar././././", ".", "../file.js" }, .posix), + @src(), + ); +} + +test "normalizeAndJoinStringLoose" { + var t = tester.Tester.t(std.heap.c_allocator); + defer t.report(@src()); + const string = []const u8; + const cwd = "/Users/jarredsumner/Code/app"; + + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/bar/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "file.js" }, .loose), + @src(), + ); + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "../file.js" }, .loose), + @src(), + ); + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "./bar", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "././././foo", "././././bar././././", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "././././bar././././", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", ".", "././././bar././././", ".", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Code/app/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "..", "././././bar././././", ".", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/bar/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "file.js" }, .loose), + @src(), + ); + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "bar", "../file.js" }, .loose), + @src(), + ); + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "foo", "./bar", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Users/jarredsumner/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ ".\\.\\.\\.\\foo", "././././bar././././", "..\\file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "././././bar././././", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Code/app/foo/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", ".", "././././bar././././", ".", "../file.js" }, .loose), + @src(), + ); + + _ = t.expect( + "/Code/app/file.js", + normalizeAndJoinString(cwd, [_]string{ "/Code/app", "././././foo", "..", "././././bar././././", ".", "../file.js" }, .loose), + @src(), + ); +} - try testResolve("/", ".."); - try testResolve("/", "/.."); - try testResolve("/", "/../../../.."); - try testResolve("/a/b/c", "a/b/c/"); +test "normalizeStringPosix" { + var t = tester.Tester.t(std.heap.c_allocator); + defer t.report(@src()); - try testResolve("/new/date.txt", "/new/../../new/date.txt"); + // Don't mess up strings that + _ = t.expect("foo/bar.txt", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar.txt", true, .posix), @src()); + _ = t.expect("foo/bar.txt", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar.txt", false, .posix), @src()); + _ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar", true, .posix), @src()); + _ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar", false, .posix), @src()); + _ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/././foo/././././././bar/../bar/../bar", true, .posix), @src()); + _ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar", false, .posix), @src()); + _ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/foo/bar//////", false, .posix), @src()); + _ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/////foo/bar//////", false, .posix), @src()); + _ = t.expect("foo/bar", try normalizeStringAlloc(std.heap.c_allocator, "/////foo/bar", false, .posix), @src()); + _ = t.expect("", try normalizeStringAlloc(std.heap.c_allocator, "/////", false, .posix), @src()); + _ = t.expect("..", try normalizeStringAlloc(std.heap.c_allocator, "../boom/../", true, .posix), @src()); + _ = t.expect("", try normalizeStringAlloc(std.heap.c_allocator, "./", true, .posix), @src()); } -test "resolvePath overflow" { - var buf: [1]u8 = undefined; +test "normalizeStringWindows" { + var t = tester.Tester.t(std.heap.c_allocator); + defer t.report(@src()); - std.testing.expectEqualStrings("/", try resolvePath(&buf, "/")); - std.testing.expectError(error.BufferTooSmall, resolvePath(&buf, "a")); // will resolve to "/a" + // Don't mess up strings that + _ = t.expect("foo\\bar.txt", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar.txt", true, .windows), @src()); + _ = t.expect("foo\\bar.txt", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar.txt", false, .windows), @src()); + _ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar", true, .windows), @src()); + _ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar", false, .windows), @src()); + _ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\.\\.\\foo\\.\\.\\.\\.\\.\\.\\bar\\..\\bar\\..\\bar", true, .windows), @src()); + _ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar", false, .windows), @src()); + _ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\foo\\bar\\\\\\\\\\\\", false, .windows), @src()); + _ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\\\\\\\\\foo\\bar\\\\\\\\\\\\", false, .windows), @src()); + _ = t.expect("foo\\bar", try normalizeStringAlloc(std.heap.c_allocator, "\\\\\\\\\\foo\\bar", false, .windows), @src()); + _ = t.expect("", try normalizeStringAlloc(std.heap.c_allocator, "\\\\\\\\\\", false, .windows), @src()); + _ = t.expect("..", try normalizeStringAlloc(std.heap.c_allocator, "..\\boom\\..\\", true, .windows), @src()); + _ = t.expect("", try normalizeStringAlloc(std.heap.c_allocator, ".\\", true, .windows), @src()); } diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 6304d032e..fd83656de 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -15,59 +15,7 @@ const hash_map_v2 = @import("../hash_map_v2.zig"); const Mutex = sync.Mutex; const StringBoolMap = std.StringHashMap(bool); -// https://en.wikipedia.org/wiki/.bss#BSS_in_C -pub fn BSSSectionAllocator(comptime size: usize) type { - const FixedBufferAllocator = std.heap.FixedBufferAllocator; - return struct { - var backing_buf: [size]u8 = undefined; - var fixed_buffer_allocator = FixedBufferAllocator.init(&backing_buf); - var buf_allocator = &fixed_buffer_allocator.allocator; - const Allocator = std.mem.Allocator; - const Self = @This(); - - allocator: Allocator, - fallback_allocator: *Allocator, - - pub fn get(self: *Self) *Allocator { - return &self.allocator; - } - - pub fn init(fallback_allocator: *Allocator) Self { - return Self{ .fallback_allocator = fallback_allocator, .allocator = Allocator{ - .allocFn = BSSSectionAllocator(size).alloc, - .resizeFn = BSSSectionAllocator(size).resize, - } }; - } - - pub fn alloc( - allocator: *Allocator, - len: usize, - ptr_align: u29, - len_align: u29, - return_address: usize, - ) error{OutOfMemory}![]u8 { - const self = @fieldParentPtr(Self, "allocator", allocator); - return buf_allocator.allocFn(buf_allocator, len, ptr_align, len_align, return_address) catch - return self.fallback_allocator.allocFn(self.fallback_allocator, len, ptr_align, len_align, return_address); - } - - pub fn resize( - allocator: *Allocator, - buf: []u8, - buf_align: u29, - new_len: usize, - len_align: u29, - return_address: usize, - ) error{OutOfMemory}!usize { - const self = @fieldParentPtr(Self, "allocator", allocator); - if (fixed_buffer_allocator.ownsPtr(buf.ptr)) { - return fixed_buffer_allocator.allocator.resizeFn(&fixed_buffer_allocator.allocator, buf, buf_align, new_len, len_align, return_address); - } else { - return self.fallback_allocator.resizeFn(self.fallback_allocator, buf, buf_align, new_len, len_align, return_address); - } - } - }; -} +const allocators = @import("../allocators.zig"); const Path = Fs.Path; @@ -84,11 +32,11 @@ pub const DirInfo = struct { // These objects are immutable, so we can just point to the parent directory // and avoid having to lock the cache again - parent: Index = HashMap.NotFound, + parent: Index = allocators.NotFound, // A pointer to the enclosing dirInfo with a valid "browser" field in // package.json. We need this to remap paths after they have been resolved. - enclosing_browser_scope: Index = HashMap.NotFound, + enclosing_browser_scope: Index = allocators.NotFound, abs_path: string = "", entries: Fs.FileSystem.DirEntry = undefined, @@ -98,14 +46,10 @@ pub const DirInfo = struct { abs_real_path: string = "", // If non-empty, this is the real absolute path resolving any symlinks pub fn getParent(i: *DirInfo) ?*DirInfo { - if (i.parent == HashMap.NotFound) return null; - std.debug.assert(i.parent < HashMap.instance._data.len); - return &HashMap.instance._data.items(.value)[i.parent]; + return HashMap.instance.atIndex(i.parent); } pub fn getEnclosingBrowserScope(i: *DirInfo) ?*DirInfo { - if (i.enclosing_browser_scope == HashMap.NotFound) return null; - std.debug.assert(i.enclosing_browser_scope < HashMap.instance._data.len); - return &HashMap.instance._data.items(.value)[i.enclosing_browser_scope]; + return HashMap.instance.atIndex(i.enclosing_browser_scope); } // Goal: Really fast, low allocation directory map exploiting cache locality where we don't worry about lifetimes much. @@ -113,98 +57,7 @@ pub const DirInfo = struct { // 2. Don't expect a provided key to exist after it's queried // 3. Store whether a directory has been queried and whether that query was successful. // 4. Allocate onto the https://en.wikipedia.org/wiki/.bss#BSS_in_C instead of the heap, so we can avoid memory leaks - pub const HashMap = struct { - // In a small next.js app with few additional dependencies, there are 191 directories in the node_modules folder - // fd . -d 9999 -L -t d --no-ignore | wc -l - const PreallocatedCount = 256; - const StringAllocatorSize = 128 * PreallocatedCount; - const FallbackStringAllocator = BSSSectionAllocator(StringAllocatorSize); - const FallbackAllocatorSize = @divExact(@bitSizeOf(Entry), 8) * PreallocatedCount; - const FallbackAllocator = BSSSectionAllocator(FallbackAllocatorSize); - const BackingHashMap = std.AutoHashMapUnmanaged(u64, Index); - pub const Entry = struct { - key: string, - value: DirInfo, - }; - string_allocator: FallbackStringAllocator, - fallback_allocator: FallbackAllocator, - allocator: *std.mem.Allocator, - _data: std.MultiArrayList(Entry), - hash_map: BackingHashMap, - const Seed = 999; - pub const NotFound: Index = std.math.maxInt(Index); - var instance: HashMap = undefined; - - pub fn at(d: *HashMap, index: Index) *DirInfo { - return &d._data.items(.value)[index]; - } - - pub const Result = struct { - index: Index = NotFound, - hash: u64 = 0, - status: Status = Status.unknown, - - pub const Status = enum { unknown, not_found, exists }; - }; - - // pub fn get(d: *HashMap, key: string) Result { - // const _key = Wyhash.hash(Seed, key); - // const index = d.hash_map.get(_key) orelse return Result{}; - - // return d._data.items(.value)[index]; - // } - - pub fn getOrPut( - d: *HashMap, - key: string, - ) Result { - const _key = Wyhash.hash(Seed, key); - const index = d.hash_map.get(_key) orelse return Result{ - .index = std.math.maxInt(u32), - .status = .unknown, - .hash = _key, - }; - if (index == NotFound) { - return Result{ .index = NotFound, .status = .not_found, .hash = _key }; - } - - return Result{ .index = index, .status = .exists, .hash = _key }; - } - - pub fn put(d: *HashMap, hash: u64, key: string, value: DirInfo) *DirInfo { - const entry = Entry{ - .value = value, - .key = d.string_allocator.get().dupe(u8, key) catch unreachable, - }; - const index = d._data.len; - d._data.append(d.fallback_allocator.get(), entry) catch unreachable; - d.hash_map.put(d.allocator, hash, @intCast(DirInfo.Index, index)) catch unreachable; - return &d._data.items(.value)[index]; - } - - pub fn init(allocator: *std.mem.Allocator) *HashMap { - var list = std.MultiArrayList(Entry){}; - instance = HashMap{ - ._data = undefined, - .string_allocator = FallbackStringAllocator.init(allocator), - .allocator = allocator, - .hash_map = BackingHashMap{}, - .fallback_allocator = FallbackAllocator.init(allocator), - }; - list.ensureTotalCapacity(instance.allocator, PreallocatedCount) catch unreachable; - instance._data = list; - return &instance; - } - - pub fn markNotFound(d: *HashMap, hash: u64) void { - d.hash_map.put(d.allocator, hash, NotFound) catch unreachable; - } - - pub fn deinit(i: *HashMap) void { - i._data.deinit(i.allocator); - i.hash_map.deinit(i.allocator); - } - }; + pub const HashMap = allocators.BSSMap(DirInfo, 1024, true, 128); }; pub const TemporaryBuffer = struct { pub threadlocal var ExtensionPathBuf = std.mem.zeroes([512]u8); @@ -578,7 +431,7 @@ pub const Resolver = struct { if (check_relative) { const parts = [_]string{ source_dir, import_path }; - const abs_path = std.fs.path.join(r.allocator, &parts) catch unreachable; + const abs_path = r.fs.join(&parts); if (r.opts.external.abs_paths.count() > 0 and r.opts.external.abs_paths.exists(abs_path)) { // If the string literal in the source text is an absolute path and has @@ -760,7 +613,7 @@ pub const Resolver = struct { // Try looking up the path relative to the base URL if (tsconfig.base_url) |base| { const paths = [_]string{ base, import_path }; - const abs = std.fs.path.join(r.allocator, &paths) catch unreachable; + const abs = r.fs.join(paths); if (r.loadAsFileOrDirectory(abs, kind)) |res| { return res; @@ -775,7 +628,7 @@ pub const Resolver = struct { // don't ever want to search for "node_modules/node_modules" if (dir_info.has_node_modules) { var _paths = [_]string{ dir_info.abs_path, "node_modules", import_path }; - const abs_path = std.fs.path.join(r.allocator, &_paths) catch unreachable; + const abs_path = r.fs.join(&_paths); if (r.debug_logs) |*debug| { debug.addNoteFmt("Checking for a package in the directory \"{s}\"", .{abs_path}) catch {}; } @@ -802,7 +655,7 @@ pub const Resolver = struct { return r.loadNodeModules(import_path, kind, source_dir_info); } else { const paths = [_]string{ source_dir_info.abs_path, import_path }; - var resolved = std.fs.path.join(r.allocator, &paths) catch unreachable; + var resolved = r.fs.join(&paths); return r.loadAsFileOrDirectory(resolved, kind); } } @@ -825,7 +678,7 @@ pub const Resolver = struct { // // // Skip "node_modules" folders // // if (!strings.eql(std.fs.path.basename(current), "node_modules")) { // // var paths1 = [_]string{ current, "node_modules", extends }; - // // var join1 = std.fs.path.join(ctx.r.allocator, &paths1) catch unreachable; + // // var join1 = r.fs.joinAlloc(ctx.r.allocator, &paths1) catch unreachable; // // const res = ctx.r.parseTSConfig(join1, ctx.visited) catch |err| { // // if (err == error.ENOENT) { // // continue; @@ -857,14 +710,14 @@ pub const Resolver = struct { // this might leak if (!std.fs.path.isAbsolute(base)) { const paths = [_]string{ file_dir, base }; - result.base_url = std.fs.path.join(r.allocator, &paths) catch unreachable; + result.base_url = r.fs.joinAlloc(r.allocator, &paths) catch unreachable; } } if (result.paths.count() > 0 and (result.base_url_for_paths.len == 0 or !std.fs.path.isAbsolute(result.base_url_for_paths))) { // this might leak const paths = [_]string{ file_dir, result.base_url.? }; - result.base_url_for_paths = std.fs.path.join(r.allocator, &paths) catch unreachable; + result.base_url_for_paths = r.fs.joinAlloc(r.allocator, &paths) catch unreachable; } return result; @@ -888,10 +741,13 @@ pub const Resolver = struct { } fn dirInfoCached(r: *Resolver, path: string) !?*DirInfo { - var dir_info_entry = r.dir_cache.getOrPut( - path, - ); + var dir_info_entry = try r.dir_cache.getOrPut(path); + + var ptr = try r.dirInfoCachedGetOrPut(path, &dir_info_entry); + return ptr; + } + fn dirInfoCachedGetOrPut(r: *Resolver, path: string, dir_info_entry: *allocators.Result) !?*DirInfo { switch (dir_info_entry.status) { .unknown => { return try r.dirInfoUncached(path, dir_info_entry); @@ -900,7 +756,7 @@ pub const Resolver = struct { return null; }, .exists => { - return r.dir_cache.at(dir_info_entry.index); + return r.dir_cache.atIndex(dir_info_entry.index); }, } // if (__entry.found_existing) { @@ -953,7 +809,7 @@ pub const Resolver = struct { if (!std.fs.path.isAbsolute(absolute_original_path)) { const parts = [_]string{ abs_base_url, original_path }; - absolute_original_path = std.fs.path.join(r.allocator, &parts) catch unreachable; + absolute_original_path = r.fs.joinAlloc(r.allocator, &parts) catch unreachable; was_alloc = true; } @@ -1032,12 +888,12 @@ pub const Resolver = struct { const region = TemporaryBuffer.TSConfigMatchPathBuf[0..total_length]; // Load the original path relative to the "baseUrl" from tsconfig.json - var absolute_original_path = region; + var absolute_original_path: string = region; var did_allocate = false; if (!std.fs.path.isAbsolute(region)) { - const paths = [_]string{ abs_base_url, original_path }; - absolute_original_path = std.fs.path.join(r.allocator, &paths) catch unreachable; + var paths = [_]string{ abs_base_url, original_path }; + absolute_original_path = r.fs.joinAlloc(r.allocator, &paths) catch unreachable; did_allocate = true; } else { absolute_original_path = std.mem.dupe(r.allocator, u8, region) catch unreachable; @@ -1059,7 +915,7 @@ pub const Resolver = struct { pub fn checkBrowserMap(r: *Resolver, pkg: *PackageJSON, input_path: string) ?string { // Normalize the path so we can compare against it without getting confused by "./" - var cleaned = Path.normalizeNoAlloc(input_path, true); + var cleaned = r.fs.normalize(input_path); const original_cleaned = cleaned; if (cleaned.len == 1 and cleaned[0] == '.') { @@ -1132,7 +988,7 @@ pub const Resolver = struct { // Is the path disabled? if (remap.len == 0) { const paths = [_]string{ path, field_rel_path }; - const new_path = std.fs.path.join(r.allocator, &paths) catch unreachable; + const new_path = r.fs.joinAlloc(r.allocator, &paths) catch unreachable; var _path = Path.init(new_path); _path.is_disabled = true; return MatchResult{ @@ -1147,7 +1003,7 @@ pub const Resolver = struct { } } const _paths = [_]string{ field_rel_path, path }; - const field_abs_path = std.fs.path.join(r.allocator, &_paths) catch unreachable; + const field_abs_path = r.fs.joinAlloc(r.allocator, &_paths) catch unreachable; const field_dir_info = (r.dirInfoCached(field_abs_path) catch null) orelse { r.allocator.free(field_abs_path); @@ -1171,7 +1027,7 @@ pub const Resolver = struct { if (dir_info.entries.get(base)) |lookup| { if (lookup.entry.kind(rfs) == .file) { const parts = [_]string{ path, base }; - const out_buf = std.fs.path.join(r.allocator, &parts) catch unreachable; + const out_buf = r.fs.joinAlloc(r.allocator, &parts) catch unreachable; if (r.debug_logs) |*debug| { debug.addNoteFmt("Found file: \"{s}\"", .{out_buf}) catch unreachable; } @@ -1197,7 +1053,7 @@ pub const Resolver = struct { // This doesn't really make sense to me. if (remap.len == 0) { const paths = [_]string{ path, field_rel_path }; - const new_path = std.fs.path.join(r.allocator, &paths) catch unreachable; + const new_path = r.fs.joinAlloc(r.allocator, &paths) catch unreachable; var _path = Path.init(new_path); _path.is_disabled = true; return MatchResult{ @@ -1208,7 +1064,7 @@ pub const Resolver = struct { } const new_paths = [_]string{ path, remap }; - const remapped_abs = std.fs.path.join(r.allocator, &new_paths) catch unreachable; + const remapped_abs = r.fs.joinAlloc(r.allocator, &new_paths) catch unreachable; // Is this a file if (r.loadAsFile(remapped_abs, extension_order)) |file_result| { @@ -1347,13 +1203,13 @@ pub const Resolver = struct { } } - // Read the directory entries once to minimize locking - const dir_path = std.fs.path.dirname(path) orelse unreachable; // Expected path to be a file. - const dir_entry: Fs.FileSystem.RealFS.EntriesOption = r.fs.fs.readDirectory(dir_path) catch { + const dir_path = std.fs.path.dirname(path) orelse "/"; + + const dir_entry: *Fs.FileSystem.RealFS.EntriesOption = rfs.readDirectory(dir_path, null, false) catch { return null; }; - if (@as(Fs.FileSystem.RealFS.EntriesOption.Tag, dir_entry) == .err) { + if (@as(Fs.FileSystem.RealFS.EntriesOption.Tag, dir_entry.*) == .err) { if (dir_entry.err.original_err != error.ENOENT) { r.log.addErrorFmt( null, @@ -1383,8 +1239,9 @@ pub const Resolver = struct { if (r.debug_logs) |*debug| { debug.addNoteFmt("Found file \"{s}\" ", .{base}) catch {}; } - - return LoadResult{ .path = path, .diff_case = query.diff_case }; + const abs_path_parts = [_]string{ query.entry.dir, query.entry.base }; + const abs_path = r.fs.joinAlloc(r.allocator, &abs_path_parts) catch unreachable; + return LoadResult{ .path = abs_path, .diff_case = query.diff_case }; } } @@ -1467,74 +1324,96 @@ pub const Resolver = struct { return null; } - fn dirInfoUncached(r: *Resolver, path: string, result: DirInfo.HashMap.Result) anyerror!?*DirInfo { + fn dirInfoUncached(r: *Resolver, unsafe_path: string, result: *allocators.Result) anyerror!?*DirInfo { var rfs: *Fs.FileSystem.RealFS = &r.fs.fs; var parent: ?*DirInfo = null; - const parent_dir = std.fs.path.dirname(path) orelse { - r.dir_cache.markNotFound(result.hash); - return null; - }; + var is_root = false; + const parent_dir = (std.fs.path.dirname(unsafe_path) orelse parent_dir_handle: { + is_root = true; + break :parent_dir_handle "/"; + }); - var parent_result: DirInfo.HashMap.Result = undefined; - if (parent_dir.len > 1 and !strings.eql(parent_dir, path)) { - parent = (try r.dirInfoCached(parent_dir)) orelse { - r.dir_cache.markNotFound(result.hash); - return null; - }; + var parent_result: allocators.Result = allocators.Result{ + .hash = std.math.maxInt(u64), + .index = allocators.NotFound, + .status = .unknown, + }; + if (!is_root and !strings.eql(parent_dir, unsafe_path)) { + parent = r.dirInfoCached(parent_dir) catch null; - parent_result = r.dir_cache.getOrPut(parent_dir); + if (parent != null) { + parent_result = try r.dir_cache.getOrPut(parent_dir); + } } + var entries: Fs.FileSystem.DirEntry = Fs.FileSystem.DirEntry.empty(unsafe_path, r.allocator); + // List the directories - var _entries = try rfs.readDirectory(path); - var entries: @TypeOf(_entries.entries) = undefined; - if (std.meta.activeTag(_entries) == .err) { - // Just pretend this directory is empty if we can't access it. This is the - // case on Unix for directories that only have the execute permission bit - // set. It means we will just pass through the empty directory and - // continue to check the directories above it, which is now node behaves. - switch (_entries.err.original_err) { - error.EACCESS => { - entries = Fs.FileSystem.DirEntry.empty(path, r.allocator); - }, + if (!is_root) { + var _entries: *Fs.FileSystem.RealFS.EntriesOption = undefined; + + _entries = try rfs.readDirectory(unsafe_path, null, true); + + if (std.meta.activeTag(_entries.*) == .err) { + // Just pretend this directory is empty if we can't access it. This is the + // case on Unix for directories that only have the execute permission bit + // set. It means we will just pass through the empty directory and + // continue to check the directories above it, which is now node behaves. + switch (_entries.err.original_err) { + error.EACCESS => { + entries = Fs.FileSystem.DirEntry.empty(unsafe_path, r.allocator); + }, - // Ignore "ENOTDIR" here so that calling "ReadDirectory" on a file behaves - // as if there is nothing there at all instead of causing an error due to - // the directory actually being a file. This is a workaround for situations - // where people try to import from a path containing a file as a parent - // directory. The "pnpm" package manager generates a faulty "NODE_PATH" - // list which contains such paths and treating them as missing means we just - // ignore them during path resolution. - error.ENOENT, - error.ENOTDIR, - => {}, - else => { - const pretty = r.prettyPath(Path.init(path)); - r.log.addErrorFmt( - null, - logger.Loc{}, - r.allocator, - "Cannot read directory \"{s}\": {s}", - .{ - pretty, - @errorName(_entries.err.original_err), - }, - ) catch {}; - r.dir_cache.markNotFound(result.hash); - return null; - }, + // Ignore "ENOTDIR" here so that calling "ReadDirectory" on a file behaves + // as if there is nothing there at all instead of causing an error due to + // the directory actually being a file. This is a workaround for situations + // where people try to import from a path containing a file as a parent + // directory. The "pnpm" package manager generates a faulty "NODE_PATH" + // list which contains such paths and treating them as missing means we just + // ignore them during path resolution. + error.ENOENT, + error.ENOTDIR, + error.IsDir, + => { + entries = Fs.FileSystem.DirEntry.empty(unsafe_path, r.allocator); + }, + else => { + const pretty = r.prettyPath(Path.init(unsafe_path)); + result.status = .not_found; + r.log.addErrorFmt( + null, + logger.Loc{}, + r.allocator, + "Cannot read directory \"{s}\": {s}", + .{ + pretty, + @errorName(_entries.err.original_err), + }, + ) catch {}; + r.dir_cache.markNotFound(result.*); + return null; + }, + } + } else { + entries = _entries.entries; } - } else { - entries = _entries.entries; } - var info = DirInfo{ - .abs_path = path, - .parent = if (parent != null) parent_result.index else DirInfo.HashMap.NotFound, - .entries = entries, + var info = dir_info_getter: { + var _info = DirInfo{ + .abs_path = "", + .parent = parent_result.index, + .entries = entries, + }; + result.status = .exists; + var __info = try r.dir_cache.put(unsafe_path, true, result, _info); + __info.abs_path = r.dir_cache.keyAtIndex(result.index).?; + break :dir_info_getter __info; }; + const path = info.abs_path; + // A "node_modules" directory isn't allowed to directly contain another "node_modules" directory var base = std.fs.path.basename(path); if (!strings.eqlComptime(base, "node_modules")) { @@ -1544,29 +1423,31 @@ pub const Resolver = struct { } } - // Propagate the browser scope into child directories - if (parent) |parent_info| { - info.enclosing_browser_scope = parent_info.enclosing_browser_scope; + if (parent_result.status != .unknown) { + // Propagate the browser scope into child directories + if (parent) |parent_info| { + info.enclosing_browser_scope = parent_info.enclosing_browser_scope; - // Make sure "absRealPath" is the real path of the directory (resolving any symlinks) - if (!r.opts.preserve_symlinks) { - if (parent_info.entries.get(base)) |lookup| { - const entry = lookup.entry; + // Make sure "absRealPath" is the real path of the directory (resolving any symlinks) + if (!r.opts.preserve_symlinks) { + if (parent_info.entries.get(base)) |lookup| { + const entry = lookup.entry; - var symlink = entry.symlink(rfs); - if (symlink.len > 0) { - if (r.debug_logs) |*logs| { - try logs.addNote(std.fmt.allocPrint(r.allocator, "Resolved symlink \"{s}\" to \"{s}\"", .{ path, symlink }) catch unreachable); - } - info.abs_real_path = symlink; - } else if (parent_info.abs_real_path.len > 0) { - // this might leak a little i'm not sure - const parts = [_]string{ parent_info.abs_real_path, base }; - symlink = std.fs.path.join(r.allocator, &parts) catch unreachable; - if (r.debug_logs) |*logs| { - try logs.addNote(std.fmt.allocPrint(r.allocator, "Resolved symlink \"{s}\" to \"{s}\"", .{ path, symlink }) catch unreachable); + var symlink = entry.symlink(rfs); + if (symlink.len > 0) { + if (r.debug_logs) |*logs| { + try logs.addNote(std.fmt.allocPrint(r.allocator, "Resolved symlink \"{s}\" to \"{s}\"", .{ path, symlink }) catch unreachable); + } + info.abs_real_path = symlink; + } else if (parent_info.abs_real_path.len > 0) { + // this might leak a little i'm not sure + const parts = [_]string{ parent_info.abs_real_path, base }; + symlink = r.fs.joinAlloc(r.allocator, &parts) catch unreachable; + if (r.debug_logs) |*logs| { + try logs.addNote(std.fmt.allocPrint(r.allocator, "Resolved symlink \"{s}\" to \"{s}\"", .{ path, symlink }) catch unreachable); + } + info.abs_real_path = symlink; } - info.abs_real_path = symlink; } } } @@ -1580,8 +1461,7 @@ pub const Resolver = struct { if (info.package_json) |pkg| { if (pkg.browser_map.count() > 0) { - // it has not been written yet, so we reserve the next index - info.enclosing_browser_scope = @intCast(DirInfo.Index, DirInfo.HashMap.instance._data.len); + info.enclosing_browser_scope = result.index; } if (r.debug_logs) |*logs| { @@ -1601,7 +1481,7 @@ pub const Resolver = struct { const entry = lookup.entry; if (entry.kind(rfs) == .file) { const parts = [_]string{ path, "tsconfig.json" }; - tsconfig_path = try std.fs.path.join(r.allocator, &parts); + tsconfig_path = try r.fs.joinAlloc(r.allocator, &parts); } } if (tsconfig_path == null) { @@ -1609,7 +1489,7 @@ pub const Resolver = struct { const entry = lookup.entry; if (entry.kind(rfs) == .file) { const parts = [_]string{ path, "jsconfig.json" }; - tsconfig_path = try std.fs.path.join(r.allocator, &parts); + tsconfig_path = try r.fs.joinAlloc(r.allocator, &parts); } } } @@ -1625,7 +1505,7 @@ pub const Resolver = struct { if (err == error.ENOENT) { r.log.addErrorFmt(null, logger.Loc.Empty, r.allocator, "Cannot find tsconfig file \"{s}\"", .{pretty}) catch unreachable; - } else if (err != error.ParseErrorAlreadyLogged) { + } else if (err != error.ParseErrorAlreadyLogged and err != error.IsDir) { r.log.addErrorFmt(null, logger.Loc.Empty, r.allocator, "Cannot read file \"{s}\": {s}", .{ pretty, @errorName(err) }) catch unreachable; } break :brk null; @@ -1637,7 +1517,6 @@ pub const Resolver = struct { info.tsconfig_json = parent.?.tsconfig_json; } - info.entries = entries; - return r.dir_cache.put(result.hash, path, info); + return info; } }; diff --git a/src/test/fixtures/exports-bug.js b/src/test/fixtures/exports-bug.js new file mode 100644 index 000000000..081b8cfa0 --- /dev/null +++ b/src/test/fixtures/exports-bug.js @@ -0,0 +1,2342 @@ +import { + RGBAFormat, + HalfFloatType, + FloatType, + UnsignedByteType, + TriangleFanDrawMode, + TriangleStripDrawMode, + TrianglesDrawMode, + LinearToneMapping, + BackSide, +} from "../constants.js"; +import { _Math } from "../math/Math.js"; +import { DataTexture } from "../textures/DataTexture.js"; +import { Frustum } from "../math/Frustum.js"; +import { Matrix4 } from "../math/Matrix4.js"; +import { ShaderLib } from "./shaders/ShaderLib.js"; +import { UniformsLib } from "./shaders/UniformsLib.js"; +import { cloneUniforms } from "./shaders/UniformsUtils.js"; +import { Vector2 } from "../math/Vector2.js"; +import { Vector3 } from "../math/Vector3.js"; +import { Vector4 } from "../math/Vector4.js"; +import { WebGLAnimation } from "./webgl/WebGLAnimation.js"; +import { WebGLAttributes } from "./webgl/WebGLAttributes.js"; +import { WebGLBackground } from "./webgl/WebGLBackground.js"; +import { WebGLBufferRenderer } from "./webgl/WebGLBufferRenderer.js"; +import { WebGLCapabilities } from "./webgl/WebGLCapabilities.js"; +import { WebGLClipping } from "./webgl/WebGLClipping.js"; +import { WebGLExtensions } from "./webgl/WebGLExtensions.js"; +import { WebGLGeometries } from "./webgl/WebGLGeometries.js"; +import { WebGLIndexedBufferRenderer } from "./webgl/WebGLIndexedBufferRenderer.js"; +import { WebGLInfo } from "./webgl/WebGLInfo.js"; +import { WebGLMorphtargets } from "./webgl/WebGLMorphtargets.js"; +import { WebGLObjects } from "./webgl/WebGLObjects.js"; +import { WebGLPrograms } from "./webgl/WebGLPrograms.js"; +import { WebGLProperties } from "./webgl/WebGLProperties.js"; +import { WebGLRenderLists } from "./webgl/WebGLRenderLists.js"; +import { WebGLRenderStates } from "./webgl/WebGLRenderStates.js"; +import { WebGLShadowMap } from "./webgl/WebGLShadowMap.js"; +import { WebGLState } from "./webgl/WebGLState.js"; +import { WebGLTextures } from "./webgl/WebGLTextures.js"; +import { WebGLUniforms } from "./webgl/WebGLUniforms.js"; +import { WebGLUtils } from "./webgl/WebGLUtils.js"; +import { WebVRManager } from "./webvr/WebVRManager.js"; +import { WebXRManager } from "./webvr/WebXRManager.js"; + +/** + * @author supereggbert / http://www.paulbrunt.co.uk/ + * @author mrdoob / http://mrdoob.com/ + * @author alteredq / http://alteredqualia.com/ + * @author szimek / https://github.com/szimek/ + * @author tschw + */ + +function WebGLRenderer(parameters) { + parameters = parameters || {}; + + var _canvas = + parameters.canvas !== undefined + ? parameters.canvas + : document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"), + _context = parameters.context !== undefined ? parameters.context : null, + _alpha = parameters.alpha !== undefined ? parameters.alpha : false, + _depth = parameters.depth !== undefined ? parameters.depth : true, + _stencil = parameters.stencil !== undefined ? parameters.stencil : true, + _antialias = + parameters.antialias !== undefined ? parameters.antialias : false, + _premultipliedAlpha = + parameters.premultipliedAlpha !== undefined + ? parameters.premultipliedAlpha + : true, + _preserveDrawingBuffer = + parameters.preserveDrawingBuffer !== undefined + ? parameters.preserveDrawingBuffer + : false, + _powerPreference = + parameters.powerPreference !== undefined + ? parameters.powerPreference + : "default", + _failIfMajorPerformanceCaveat = + parameters.failIfMajorPerformanceCaveat !== undefined + ? parameters.failIfMajorPerformanceCaveat + : false; + + var currentRenderList = null; + var currentRenderState = null; + + // public properties + + this.domElement = _canvas; + + // Debug configuration container + this.debug = { + /** + * Enables error checking and reporting when shader programs are being compiled + * @type {boolean} + */ + checkShaderErrors: true, + }; + + // clearing + + this.autoClear = true; + this.autoClearColor = true; + this.autoClearDepth = true; + this.autoClearStencil = true; + + // scene graph + + this.sortObjects = true; + + // user-defined clipping + + this.clippingPlanes = []; + this.localClippingEnabled = false; + + // physically based shading + + this.gammaFactor = 2.0; // for backwards compatibility + this.gammaInput = false; + this.gammaOutput = false; + + // physical lights + + this.physicallyCorrectLights = false; + + // tone mapping + + this.toneMapping = LinearToneMapping; + this.toneMappingExposure = 1.0; + this.toneMappingWhitePoint = 1.0; + + // morphs + + this.maxMorphTargets = 8; + this.maxMorphNormals = 4; + + // internal properties + + var _this = this, + _isContextLost = false, + // internal state cache + + _framebuffer = null, + _currentActiveCubeFace = 0, + _currentActiveMipmapLevel = 0, + _currentRenderTarget = null, + _currentFramebuffer = null, + _currentMaterialId = -1, + // geometry and program caching + + _currentGeometryProgram = { + geometry: null, + program: null, + wireframe: false, + }, + _currentCamera = null, + _currentArrayCamera = null, + _currentViewport = new Vector4(), + _currentScissor = new Vector4(), + _currentScissorTest = null, + // + + _width = _canvas.width, + _height = _canvas.height, + _pixelRatio = 1, + _viewport = new Vector4(0, 0, _width, _height), + _scissor = new Vector4(0, 0, _width, _height), + _scissorTest = false, + // frustum + + _frustum = new Frustum(), + // clipping + + _clipping = new WebGLClipping(), + _clippingEnabled = false, + _localClippingEnabled = false, + // camera matrices cache + + _projScreenMatrix = new Matrix4(), + _vector3 = new Vector3(); + + function getTargetPixelRatio() { + return _currentRenderTarget === null ? _pixelRatio : 1; + } + + // initialize + + var _gl; + + try { + var contextAttributes = { + alpha: _alpha, + depth: _depth, + stencil: _stencil, + antialias: _antialias, + premultipliedAlpha: _premultipliedAlpha, + preserveDrawingBuffer: _preserveDrawingBuffer, + powerPreference: _powerPreference, + failIfMajorPerformanceCaveat: _failIfMajorPerformanceCaveat, + xrCompatible: true, + }; + + // event listeners must be registered before WebGL context is created, see #12753 + + _canvas.addEventListener("webglcontextlost", onContextLost, false); + _canvas.addEventListener("webglcontextrestored", onContextRestore, false); + + _gl = + _context || + _canvas.getContext("webgl", contextAttributes) || + _canvas.getContext("experimental-webgl", contextAttributes); + + if (_gl === null) { + if (_canvas.getContext("webgl") !== null) { + throw new Error( + "Error creating WebGL context with your selected attributes." + ); + } else { + throw new Error("Error creating WebGL context."); + } + } + + // Some experimental-webgl implementations do not have getShaderPrecisionFormat + + if (_gl.getShaderPrecisionFormat === undefined) { + _gl.getShaderPrecisionFormat = function () { + return { rangeMin: 1, rangeMax: 1, precision: 1 }; + }; + } + } catch (error) { + console.error("THREE.WebGLRenderer: " + error.message); + throw error; + } + + var extensions, capabilities, state, info; + var properties, textures, attributes, geometries, objects; + var programCache, renderLists, renderStates; + + var background, morphtargets, bufferRenderer, indexedBufferRenderer; + + var utils; + + function initGLContext() { + extensions = new WebGLExtensions(_gl); + + capabilities = new WebGLCapabilities(_gl, extensions, parameters); + + if (!capabilities.isWebGL2) { + extensions.get("WEBGL_depth_texture"); + extensions.get("OES_texture_float"); + extensions.get("OES_texture_half_float"); + extensions.get("OES_texture_half_float_linear"); + extensions.get("OES_standard_derivatives"); + extensions.get("OES_element_index_uint"); + extensions.get("ANGLE_instanced_arrays"); + } + + extensions.get("OES_texture_float_linear"); + + utils = new WebGLUtils(_gl, extensions, capabilities); + + state = new WebGLState(_gl, extensions, utils, capabilities); + state.scissor( + _currentScissor.copy(_scissor).multiplyScalar(_pixelRatio).floor() + ); + state.viewport( + _currentViewport.copy(_viewport).multiplyScalar(_pixelRatio).floor() + ); + + info = new WebGLInfo(_gl); + properties = new WebGLProperties(); + textures = new WebGLTextures( + _gl, + extensions, + state, + properties, + capabilities, + utils, + info + ); + attributes = new WebGLAttributes(_gl); + geometries = new WebGLGeometries(_gl, attributes, info); + objects = new WebGLObjects(geometries, info); + morphtargets = new WebGLMorphtargets(_gl); + programCache = new WebGLPrograms(_this, extensions, capabilities); + renderLists = new WebGLRenderLists(); + renderStates = new WebGLRenderStates(); + + background = new WebGLBackground( + _this, + state, + objects, + _premultipliedAlpha + ); + + bufferRenderer = new WebGLBufferRenderer( + _gl, + extensions, + info, + capabilities + ); + indexedBufferRenderer = new WebGLIndexedBufferRenderer( + _gl, + extensions, + info, + capabilities + ); + + info.programs = programCache.programs; + + _this.capabilities = capabilities; + _this.extensions = extensions; + _this.properties = properties; + _this.renderLists = renderLists; + _this.state = state; + _this.info = info; + } + + initGLContext(); + + // vr + + var vr = + typeof navigator !== "undefined" && + "xr" in navigator && + "supportsSession" in navigator.xr + ? new WebXRManager(_this, _gl) + : new WebVRManager(_this); + + this.vr = vr; + + // shadow map + + var shadowMap = new WebGLShadowMap( + _this, + objects, + capabilities.maxTextureSize + ); + + this.shadowMap = shadowMap; + + // API + + this.getContext = function () { + return _gl; + }; + + this.getContextAttributes = function () { + return _gl.getContextAttributes(); + }; + + this.forceContextLoss = function () { + var extension = extensions.get("WEBGL_lose_context"); + if (extension) extension.loseContext(); + }; + + this.forceContextRestore = function () { + var extension = extensions.get("WEBGL_lose_context"); + if (extension) extension.restoreContext(); + }; + + this.getPixelRatio = function () { + return _pixelRatio; + }; + + this.setPixelRatio = function (value) { + if (value === undefined) return; + + _pixelRatio = value; + + this.setSize(_width, _height, false); + }; + + this.getSize = function (target) { + if (target === undefined) { + console.warn( + "WebGLRenderer: .getsize() now requires a Vector2 as an argument" + ); + + target = new Vector2(); + } + + return target.set(_width, _height); + }; + + this.setSize = function (width, height, updateStyle) { + if (vr.isPresenting()) { + console.warn( + "THREE.WebGLRenderer: Can't change size while VR device is presenting." + ); + return; + } + + _width = width; + _height = height; + + _canvas.width = Math.floor(width * _pixelRatio); + _canvas.height = Math.floor(height * _pixelRatio); + + if (updateStyle !== false) { + _canvas.style.width = width + "px"; + _canvas.style.height = height + "px"; + } + + this.setViewport(0, 0, width, height); + }; + + this.getDrawingBufferSize = function (target) { + if (target === undefined) { + console.warn( + "WebGLRenderer: .getdrawingBufferSize() now requires a Vector2 as an argument" + ); + + target = new Vector2(); + } + + return target.set(_width * _pixelRatio, _height * _pixelRatio).floor(); + }; + + this.setDrawingBufferSize = function (width, height, pixelRatio) { + _width = width; + _height = height; + + _pixelRatio = pixelRatio; + + _canvas.width = Math.floor(width * pixelRatio); + _canvas.height = Math.floor(height * pixelRatio); + + this.setViewport(0, 0, width, height); + }; + + this.getCurrentViewport = function (target) { + if (target === undefined) { + console.warn( + "WebGLRenderer: .getCurrentViewport() now requires a Vector4 as an argument" + ); + + target = new Vector4(); + } + + return target.copy(_currentViewport); + }; + + this.getViewport = function (target) { + return target.copy(_viewport); + }; + + this.setViewport = function (x, y, width, height) { + if (x.isVector4) { + _viewport.set(x.x, x.y, x.z, x.w); + } else { + _viewport.set(x, y, width, height); + } + + state.viewport( + _currentViewport.copy(_viewport).multiplyScalar(_pixelRatio).floor() + ); + }; + + this.getScissor = function (target) { + return target.copy(_scissor); + }; + + this.setScissor = function (x, y, width, height) { + if (x.isVector4) { + _scissor.set(x.x, x.y, x.z, x.w); + } else { + _scissor.set(x, y, width, height); + } + + state.scissor( + _currentScissor.copy(_scissor).multiplyScalar(_pixelRatio).floor() + ); + }; + + this.getScissorTest = function () { + return _scissorTest; + }; + + this.setScissorTest = function (boolean) { + state.setScissorTest((_scissorTest = boolean)); + }; + + // Clearing + + this.getClearColor = function () { + return background.getClearColor(); + }; + + this.setClearColor = function () { + background.setClearColor.apply(background, arguments); + }; + + this.getClearAlpha = function () { + return background.getClearAlpha(); + }; + + this.setClearAlpha = function () { + background.setClearAlpha.apply(background, arguments); + }; + + this.clear = function (color, depth, stencil) { + var bits = 0; + + if (color === undefined || color) bits |= _gl.COLOR_BUFFER_BIT; + if (depth === undefined || depth) bits |= _gl.DEPTH_BUFFER_BIT; + if (stencil === undefined || stencil) bits |= _gl.STENCIL_BUFFER_BIT; + + _gl.clear(bits); + }; + + this.clearColor = function () { + this.clear(true, false, false); + }; + + this.clearDepth = function () { + this.clear(false, true, false); + }; + + this.clearStencil = function () { + this.clear(false, false, true); + }; + + // + + this.dispose = function () { + _canvas.removeEventListener("webglcontextlost", onContextLost, false); + _canvas.removeEventListener( + "webglcontextrestored", + onContextRestore, + false + ); + + renderLists.dispose(); + renderStates.dispose(); + properties.dispose(); + objects.dispose(); + + vr.dispose(); + + animation.stop(); + }; + + // Events + + function onContextLost(event) { + event.preventDefault(); + + console.log("THREE.WebGLRenderer: Context Lost."); + + _isContextLost = true; + } + + function onContextRestore(/* event */) { + console.log("THREE.WebGLRenderer: Context Restored."); + + _isContextLost = false; + + initGLContext(); + } + + function onMaterialDispose(event) { + var material = event.target; + + material.removeEventListener("dispose", onMaterialDispose); + + deallocateMaterial(material); + } + + // Buffer deallocation + + function deallocateMaterial(material) { + releaseMaterialProgramReference(material); + + properties.remove(material); + } + + function releaseMaterialProgramReference(material) { + var programInfo = properties.get(material).program; + + material.program = undefined; + + if (programInfo !== undefined) { + programCache.releaseProgram(programInfo); + } + } + + // Buffer rendering + + function renderObjectImmediate(object, program) { + object.render(function (object) { + _this.renderBufferImmediate(object, program); + }); + } + + this.renderBufferImmediate = function (object, program) { + state.initAttributes(); + + var buffers = properties.get(object); + + if (object.hasPositions && !buffers.position) + buffers.position = _gl.createBuffer(); + if (object.hasNormals && !buffers.normal) + buffers.normal = _gl.createBuffer(); + if (object.hasUvs && !buffers.uv) buffers.uv = _gl.createBuffer(); + if (object.hasColors && !buffers.color) buffers.color = _gl.createBuffer(); + + var programAttributes = program.getAttributes(); + + if (object.hasPositions) { + _gl.bindBuffer(_gl.ARRAY_BUFFER, buffers.position); + _gl.bufferData(_gl.ARRAY_BUFFER, object.positionArray, _gl.DYNAMIC_DRAW); + + state.enableAttribute(programAttributes.position); + _gl.vertexAttribPointer( + programAttributes.position, + 3, + _gl.FLOAT, + false, + 0, + 0 + ); + } + + if (object.hasNormals) { + _gl.bindBuffer(_gl.ARRAY_BUFFER, buffers.normal); + _gl.bufferData(_gl.ARRAY_BUFFER, object.normalArray, _gl.DYNAMIC_DRAW); + + state.enableAttribute(programAttributes.normal); + _gl.vertexAttribPointer( + programAttributes.normal, + 3, + _gl.FLOAT, + false, + 0, + 0 + ); + } + + if (object.hasUvs) { + _gl.bindBuffer(_gl.ARRAY_BUFFER, buffers.uv); + _gl.bufferData(_gl.ARRAY_BUFFER, object.uvArray, _gl.DYNAMIC_DRAW); + + state.enableAttribute(programAttributes.uv); + _gl.vertexAttribPointer(programAttributes.uv, 2, _gl.FLOAT, false, 0, 0); + } + + if (object.hasColors) { + _gl.bindBuffer(_gl.ARRAY_BUFFER, buffers.color); + _gl.bufferData(_gl.ARRAY_BUFFER, object.colorArray, _gl.DYNAMIC_DRAW); + + state.enableAttribute(programAttributes.color); + _gl.vertexAttribPointer( + programAttributes.color, + 3, + _gl.FLOAT, + false, + 0, + 0 + ); + } + + state.disableUnusedAttributes(); + + _gl.drawArrays(_gl.TRIANGLES, 0, object.count); + + object.count = 0; + }; + + this.renderBufferDirect = function ( + camera, + fog, + geometry, + material, + object, + group + ) { + var frontFaceCW = object.isMesh && object.matrixWorld.determinant() < 0; + + state.setMaterial(material, frontFaceCW); + + var program = setProgram(camera, fog, material, object); + + var updateBuffers = false; + + if ( + _currentGeometryProgram.geometry !== geometry.id || + _currentGeometryProgram.program !== program.id || + _currentGeometryProgram.wireframe !== (material.wireframe === true) + ) { + _currentGeometryProgram.geometry = geometry.id; + _currentGeometryProgram.program = program.id; + _currentGeometryProgram.wireframe = material.wireframe === true; + updateBuffers = true; + } + + if (object.morphTargetInfluences) { + morphtargets.update(object, geometry, material, program); + + updateBuffers = true; + } + + // + + var index = geometry.index; + var position = geometry.attributes.position; + var rangeFactor = 1; + + if (material.wireframe === true) { + index = geometries.getWireframeAttribute(geometry); + rangeFactor = 2; + } + + var attribute; + var renderer = bufferRenderer; + + if (index !== null) { + attribute = attributes.get(index); + + renderer = indexedBufferRenderer; + renderer.setIndex(attribute); + } + + if (updateBuffers) { + setupVertexAttributes(material, program, geometry); + + if (index !== null) { + _gl.bindBuffer(_gl.ELEMENT_ARRAY_BUFFER, attribute.buffer); + } + } + + // + + var dataCount = Infinity; + + if (index !== null) { + dataCount = index.count; + } else if (position !== undefined) { + dataCount = position.count; + } + + var rangeStart = geometry.drawRange.start * rangeFactor; + var rangeCount = geometry.drawRange.count * rangeFactor; + + var groupStart = group !== null ? group.start * rangeFactor : 0; + var groupCount = group !== null ? group.count * rangeFactor : Infinity; + + var drawStart = Math.max(rangeStart, groupStart); + var drawEnd = + Math.min(dataCount, rangeStart + rangeCount, groupStart + groupCount) - 1; + + var drawCount = Math.max(0, drawEnd - drawStart + 1); + + if (drawCount === 0) return; + + // + + if (object.isMesh) { + if (material.wireframe === true) { + state.setLineWidth(material.wireframeLinewidth * getTargetPixelRatio()); + renderer.setMode(_gl.LINES); + } else { + switch (object.drawMode) { + case TrianglesDrawMode: + renderer.setMode(_gl.TRIANGLES); + break; + + case TriangleStripDrawMode: + renderer.setMode(_gl.TRIANGLE_STRIP); + break; + + case TriangleFanDrawMode: + renderer.setMode(_gl.TRIANGLE_FAN); + break; + } + } + } else if (object.isLine) { + var lineWidth = material.linewidth; + + if (lineWidth === undefined) lineWidth = 1; // Not using Line*Material + + state.setLineWidth(lineWidth * getTargetPixelRatio()); + + if (object.isLineSegments) { + renderer.setMode(_gl.LINES); + } else if (object.isLineLoop) { + renderer.setMode(_gl.LINE_LOOP); + } else { + renderer.setMode(_gl.LINE_STRIP); + } + } else if (object.isPoints) { + renderer.setMode(_gl.POINTS); + } else if (object.isSprite) { + renderer.setMode(_gl.TRIANGLES); + } + + if (geometry && geometry.isInstancedBufferGeometry) { + if (geometry.maxInstancedCount > 0) { + renderer.renderInstances(geometry, drawStart, drawCount); + } + } else { + renderer.render(drawStart, drawCount); + } + }; + + function setupVertexAttributes(material, program, geometry) { + if ( + geometry && + geometry.isInstancedBufferGeometry && + !capabilities.isWebGL2 + ) { + if (extensions.get("ANGLE_instanced_arrays") === null) { + console.error( + "THREE.WebGLRenderer.setupVertexAttributes: using THREE.InstancedBufferGeometry but hardware does not support extension ANGLE_instanced_arrays." + ); + return; + } + } + + state.initAttributes(); + + var geometryAttributes = geometry.attributes; + + var programAttributes = program.getAttributes(); + + var materialDefaultAttributeValues = material.defaultAttributeValues; + + for (var name in programAttributes) { + var programAttribute = programAttributes[name]; + + if (programAttribute >= 0) { + var geometryAttribute = geometryAttributes[name]; + + if (geometryAttribute !== undefined) { + var normalized = geometryAttribute.normalized; + var size = geometryAttribute.itemSize; + + var attribute = attributes.get(geometryAttribute); + + // TODO Attribute may not be available on context restore + + if (attribute === undefined) continue; + + var buffer = attribute.buffer; + var type = attribute.type; + var bytesPerElement = attribute.bytesPerElement; + + if (geometryAttribute.isInterleavedBufferAttribute) { + var data = geometryAttribute.data; + var stride = data.stride; + var offset = geometryAttribute.offset; + + if (data && data.isInstancedInterleavedBuffer) { + state.enableAttributeAndDivisor( + programAttribute, + data.meshPerAttribute + ); + + if (geometry.maxInstancedCount === undefined) { + geometry.maxInstancedCount = data.meshPerAttribute * data.count; + } + } else { + state.enableAttribute(programAttribute); + } + + _gl.bindBuffer(_gl.ARRAY_BUFFER, buffer); + _gl.vertexAttribPointer( + programAttribute, + size, + type, + normalized, + stride * bytesPerElement, + offset * bytesPerElement + ); + } else { + if (geometryAttribute.isInstancedBufferAttribute) { + state.enableAttributeAndDivisor( + programAttribute, + geometryAttribute.meshPerAttribute + ); + + if (geometry.maxInstancedCount === undefined) { + geometry.maxInstancedCount = + geometryAttribute.meshPerAttribute * geometryAttribute.count; + } + } else { + state.enableAttribute(programAttribute); + } + + _gl.bindBuffer(_gl.ARRAY_BUFFER, buffer); + _gl.vertexAttribPointer( + programAttribute, + size, + type, + normalized, + 0, + 0 + ); + } + } else if (materialDefaultAttributeValues !== undefined) { + var value = materialDefaultAttributeValues[name]; + + if (value !== undefined) { + switch (value.length) { + case 2: + _gl.vertexAttrib2fv(programAttribute, value); + break; + + case 3: + _gl.vertexAttrib3fv(programAttribute, value); + break; + + case 4: + _gl.vertexAttrib4fv(programAttribute, value); + break; + + default: + _gl.vertexAttrib1fv(programAttribute, value); + } + } + } + } + } + + state.disableUnusedAttributes(); + } + + // Compile + + this.compile = function (scene, camera) { + currentRenderState = renderStates.get(scene, camera); + currentRenderState.init(); + + scene.traverse(function (object) { + if (object.isLight) { + currentRenderState.pushLight(object); + + if (object.castShadow) { + currentRenderState.pushShadow(object); + } + } + }); + + currentRenderState.setupLights(camera); + + scene.traverse(function (object) { + if (object.material) { + if (Array.isArray(object.material)) { + for (var i = 0; i < object.material.length; i++) { + initMaterial(object.material[i], scene.fog, object); + } + } else { + initMaterial(object.material, scene.fog, object); + } + } + }); + }; + + // Animation Loop + + var onAnimationFrameCallback = null; + + function onAnimationFrame(time) { + if (vr.isPresenting()) return; + if (onAnimationFrameCallback) onAnimationFrameCallback(time); + } + + var animation = new WebGLAnimation(); + animation.setAnimationLoop(onAnimationFrame); + + if (typeof window !== "undefined") animation.setContext(window); + + this.setAnimationLoop = function (callback) { + onAnimationFrameCallback = callback; + vr.setAnimationLoop(callback); + + animation.start(); + }; + + // Rendering + + this.render = function (scene, camera) { + var renderTarget, forceClear; + + if (arguments[2] !== undefined) { + console.warn( + "THREE.WebGLRenderer.render(): the renderTarget argument has been removed. Use .setRenderTarget() instead." + ); + renderTarget = arguments[2]; + } + + if (arguments[3] !== undefined) { + console.warn( + "THREE.WebGLRenderer.render(): the forceClear argument has been removed. Use .clear() instead." + ); + forceClear = arguments[3]; + } + + if (!(camera && camera.isCamera)) { + console.error( + "THREE.WebGLRenderer.render: camera is not an instance of THREE.Camera." + ); + return; + } + + if (_isContextLost) return; + + // reset caching for this frame + + _currentGeometryProgram.geometry = null; + _currentGeometryProgram.program = null; + _currentGeometryProgram.wireframe = false; + _currentMaterialId = -1; + _currentCamera = null; + + // update scene graph + + if (scene.autoUpdate === true) scene.updateMatrixWorld(); + + // update camera matrices and frustum + + if (camera.parent === null) camera.updateMatrixWorld(); + + if (vr.enabled) { + camera = vr.getCamera(camera); + } + + // + + currentRenderState = renderStates.get(scene, camera); + currentRenderState.init(); + + scene.onBeforeRender( + _this, + scene, + camera, + renderTarget || _currentRenderTarget + ); + + _projScreenMatrix.multiplyMatrices( + camera.projectionMatrix, + camera.matrixWorldInverse + ); + _frustum.setFromMatrix(_projScreenMatrix); + + _localClippingEnabled = this.localClippingEnabled; + _clippingEnabled = _clipping.init( + this.clippingPlanes, + _localClippingEnabled, + camera + ); + + currentRenderList = renderLists.get(scene, camera); + currentRenderList.init(); + + projectObject(scene, camera, 0, _this.sortObjects); + + if (_this.sortObjects === true) { + currentRenderList.sort(); + } + + // + + if (_clippingEnabled) _clipping.beginShadows(); + + var shadowsArray = currentRenderState.state.shadowsArray; + + shadowMap.render(shadowsArray, scene, camera); + + currentRenderState.setupLights(camera); + + if (_clippingEnabled) _clipping.endShadows(); + + // + + if (this.info.autoReset) this.info.reset(); + + if (renderTarget !== undefined) { + this.setRenderTarget(renderTarget); + } + + // + + background.render(currentRenderList, scene, camera, forceClear); + + // render scene + + var opaqueObjects = currentRenderList.opaque; + var transparentObjects = currentRenderList.transparent; + + if (scene.overrideMaterial) { + var overrideMaterial = scene.overrideMaterial; + + if (opaqueObjects.length) + renderObjects(opaqueObjects, scene, camera, overrideMaterial); + if (transparentObjects.length) + renderObjects(transparentObjects, scene, camera, overrideMaterial); + } else { + // opaque pass (front-to-back order) + + if (opaqueObjects.length) renderObjects(opaqueObjects, scene, camera); + + // transparent pass (back-to-front order) + + if (transparentObjects.length) + renderObjects(transparentObjects, scene, camera); + } + + // + + scene.onAfterRender(_this, scene, camera); + + // + + if (_currentRenderTarget !== null) { + // Generate mipmap if we're using any kind of mipmap filtering + + textures.updateRenderTargetMipmap(_currentRenderTarget); + + // resolve multisample renderbuffers to a single-sample texture if necessary + + textures.updateMultisampleRenderTarget(_currentRenderTarget); + } + + // Ensure depth buffer writing is enabled so it can be cleared on next render + + state.buffers.depth.setTest(true); + state.buffers.depth.setMask(true); + state.buffers.color.setMask(true); + + state.setPolygonOffset(false); + + if (vr.enabled) { + vr.submitFrame(); + } + + // _gl.finish(); + + currentRenderList = null; + currentRenderState = null; + }; + + function projectObject(object, camera, groupOrder, sortObjects) { + if (object.visible === false) return; + + var visible = object.layers.test(camera.layers); + + if (visible) { + if (object.isGroup) { + groupOrder = object.renderOrder; + } else if (object.isLOD) { + if (object.autoUpdate === true) object.update(camera); + } else if (object.isLight) { + currentRenderState.pushLight(object); + + if (object.castShadow) { + currentRenderState.pushShadow(object); + } + } else if (object.isSprite) { + if (!object.frustumCulled || _frustum.intersectsSprite(object)) { + if (sortObjects) { + _vector3 + .setFromMatrixPosition(object.matrixWorld) + .applyMatrix4(_projScreenMatrix); + } + + var geometry = objects.update(object); + var material = object.material; + + if (material.visible) { + currentRenderList.push( + object, + geometry, + material, + groupOrder, + _vector3.z, + null + ); + } + } + } else if (object.isImmediateRenderObject) { + if (sortObjects) { + _vector3 + .setFromMatrixPosition(object.matrixWorld) + .applyMatrix4(_projScreenMatrix); + } + + currentRenderList.push( + object, + null, + object.material, + groupOrder, + _vector3.z, + null + ); + } else if (object.isMesh || object.isLine || object.isPoints) { + if (object.isSkinnedMesh) { + object.skeleton.update(); + } + + if (!object.frustumCulled || _frustum.intersectsObject(object)) { + if (sortObjects) { + _vector3 + .setFromMatrixPosition(object.matrixWorld) + .applyMatrix4(_projScreenMatrix); + } + + var geometry = objects.update(object); + var material = object.material; + + if (Array.isArray(material)) { + var groups = geometry.groups; + + for (var i = 0, l = groups.length; i < l; i++) { + var group = groups[i]; + var groupMaterial = material[group.materialIndex]; + + if (groupMaterial && groupMaterial.visible) { + currentRenderList.push( + object, + geometry, + groupMaterial, + groupOrder, + _vector3.z, + group + ); + } + } + } else if (material.visible) { + currentRenderList.push( + object, + geometry, + material, + groupOrder, + _vector3.z, + null + ); + } + } + } + } + + var children = object.children; + + for (var i = 0, l = children.length; i < l; i++) { + projectObject(children[i], camera, groupOrder, sortObjects); + } + } + + function renderObjects(renderList, scene, camera, overrideMaterial) { + for (var i = 0, l = renderList.length; i < l; i++) { + var renderItem = renderList[i]; + + var object = renderItem.object; + var geometry = renderItem.geometry; + var material = + overrideMaterial === undefined ? renderItem.material : overrideMaterial; + var group = renderItem.group; + + if (camera.isArrayCamera) { + _currentArrayCamera = camera; + + var cameras = camera.cameras; + + for (var j = 0, jl = cameras.length; j < jl; j++) { + var camera2 = cameras[j]; + + if (object.layers.test(camera2.layers)) { + state.viewport(_currentViewport.copy(camera2.viewport)); + + currentRenderState.setupLights(camera2); + + renderObject(object, scene, camera2, geometry, material, group); + } + } + } else { + _currentArrayCamera = null; + + renderObject(object, scene, camera, geometry, material, group); + } + } + } + + function renderObject(object, scene, camera, geometry, material, group) { + object.onBeforeRender(_this, scene, camera, geometry, material, group); + currentRenderState = renderStates.get(scene, _currentArrayCamera || camera); + + object.modelViewMatrix.multiplyMatrices( + camera.matrixWorldInverse, + object.matrixWorld + ); + object.normalMatrix.getNormalMatrix(object.modelViewMatrix); + + if (object.isImmediateRenderObject) { + state.setMaterial(material); + + var program = setProgram(camera, scene.fog, material, object); + + _currentGeometryProgram.geometry = null; + _currentGeometryProgram.program = null; + _currentGeometryProgram.wireframe = false; + + renderObjectImmediate(object, program); + } else { + _this.renderBufferDirect( + camera, + scene.fog, + geometry, + material, + object, + group + ); + } + + object.onAfterRender(_this, scene, camera, geometry, material, group); + currentRenderState = renderStates.get(scene, _currentArrayCamera || camera); + } + + function initMaterial(material, fog, object) { + var materialProperties = properties.get(material); + + var lights = currentRenderState.state.lights; + var shadowsArray = currentRenderState.state.shadowsArray; + + var lightsStateVersion = lights.state.version; + + var parameters = programCache.getParameters( + material, + lights.state, + shadowsArray, + fog, + _clipping.numPlanes, + _clipping.numIntersection, + object + ); + + var code = programCache.getProgramCode(material, parameters); + + var program = materialProperties.program; + var programChange = true; + + if (program === undefined) { + // new material + material.addEventListener("dispose", onMaterialDispose); + } else if (program.code !== code) { + // changed glsl or parameters + releaseMaterialProgramReference(material); + } else if (materialProperties.lightsStateVersion !== lightsStateVersion) { + materialProperties.lightsStateVersion = lightsStateVersion; + + programChange = false; + } else if (parameters.shaderID !== undefined) { + // same glsl and uniform list + return; + } else { + // only rebuild uniform list + programChange = false; + } + + if (programChange) { + if (parameters.shaderID) { + var shader = ShaderLib[parameters.shaderID]; + + materialProperties.shader = { + name: material.type, + uniforms: cloneUniforms(shader.uniforms), + vertexShader: shader.vertexShader, + fragmentShader: shader.fragmentShader, + }; + } else { + materialProperties.shader = { + name: material.type, + uniforms: material.uniforms, + vertexShader: material.vertexShader, + fragmentShader: material.fragmentShader, + }; + } + + material.onBeforeCompile(materialProperties.shader, _this); + + // Computing code again as onBeforeCompile may have changed the shaders + code = programCache.getProgramCode(material, parameters); + + program = programCache.acquireProgram( + material, + materialProperties.shader, + parameters, + code + ); + + materialProperties.program = program; + material.program = program; + } + + var programAttributes = program.getAttributes(); + + if (material.morphTargets) { + material.numSupportedMorphTargets = 0; + + for (var i = 0; i < _this.maxMorphTargets; i++) { + if (programAttributes["morphTarget" + i] >= 0) { + material.numSupportedMorphTargets++; + } + } + } + + if (material.morphNormals) { + material.numSupportedMorphNormals = 0; + + for (var i = 0; i < _this.maxMorphNormals; i++) { + if (programAttributes["morphNormal" + i] >= 0) { + material.numSupportedMorphNormals++; + } + } + } + + var uniforms = materialProperties.shader.uniforms; + + if ( + (!material.isShaderMaterial && !material.isRawShaderMaterial) || + material.clipping === true + ) { + materialProperties.numClippingPlanes = _clipping.numPlanes; + materialProperties.numIntersection = _clipping.numIntersection; + uniforms.clippingPlanes = _clipping.uniform; + } + + materialProperties.fog = fog; + + // store the light setup it was created for + + materialProperties.lightsStateVersion = lightsStateVersion; + + if (material.lights) { + // wire up the material to this renderer's lighting state + + uniforms.ambientLightColor.value = lights.state.ambient; + uniforms.lightProbe.value = lights.state.probe; + uniforms.directionalLights.value = lights.state.directional; + uniforms.spotLights.value = lights.state.spot; + uniforms.rectAreaLights.value = lights.state.rectArea; + uniforms.pointLights.value = lights.state.point; + uniforms.hemisphereLights.value = lights.state.hemi; + + uniforms.directionalShadowMap.value = lights.state.directionalShadowMap; + uniforms.directionalShadowMatrix.value = + lights.state.directionalShadowMatrix; + uniforms.spotShadowMap.value = lights.state.spotShadowMap; + uniforms.spotShadowMatrix.value = lights.state.spotShadowMatrix; + uniforms.pointShadowMap.value = lights.state.pointShadowMap; + uniforms.pointShadowMatrix.value = lights.state.pointShadowMatrix; + // TODO (abelnation): add area lights shadow info to uniforms + } + + var progUniforms = materialProperties.program.getUniforms(), + uniformsList = WebGLUniforms.seqWithValue(progUniforms.seq, uniforms); + + materialProperties.uniformsList = uniformsList; + } + + function setProgram(camera, fog, material, object) { + textures.resetTextureUnits(); + + var materialProperties = properties.get(material); + var lights = currentRenderState.state.lights; + + if (_clippingEnabled) { + if (_localClippingEnabled || camera !== _currentCamera) { + var useCache = + camera === _currentCamera && material.id === _currentMaterialId; + + // we might want to call this function with some ClippingGroup + // object instead of the material, once it becomes feasible + // (#8465, #8379) + _clipping.setState( + material.clippingPlanes, + material.clipIntersection, + material.clipShadows, + camera, + materialProperties, + useCache + ); + } + } + + if (material.needsUpdate === false) { + if (materialProperties.program === undefined) { + material.needsUpdate = true; + } else if (material.fog && materialProperties.fog !== fog) { + material.needsUpdate = true; + } else if ( + material.lights && + materialProperties.lightsStateVersion !== lights.state.version + ) { + material.needsUpdate = true; + } else if ( + materialProperties.numClippingPlanes !== undefined && + (materialProperties.numClippingPlanes !== _clipping.numPlanes || + materialProperties.numIntersection !== _clipping.numIntersection) + ) { + material.needsUpdate = true; + } + } + + if (material.needsUpdate) { + initMaterial(material, fog, object); + material.needsUpdate = false; + } + + var refreshProgram = false; + var refreshMaterial = false; + var refreshLights = false; + + var program = materialProperties.program, + p_uniforms = program.getUniforms(), + m_uniforms = materialProperties.shader.uniforms; + + if (state.useProgram(program.program)) { + refreshProgram = true; + refreshMaterial = true; + refreshLights = true; + } + + if (material.id !== _currentMaterialId) { + _currentMaterialId = material.id; + + refreshMaterial = true; + } + + if (refreshProgram || _currentCamera !== camera) { + p_uniforms.setValue(_gl, "projectionMatrix", camera.projectionMatrix); + + if (capabilities.logarithmicDepthBuffer) { + p_uniforms.setValue( + _gl, + "logDepthBufFC", + 2.0 / (Math.log(camera.far + 1.0) / Math.LN2) + ); + } + + if (_currentCamera !== camera) { + _currentCamera = camera; + + // lighting uniforms depend on the camera so enforce an update + // now, in case this material supports lights - or later, when + // the next material that does gets activated: + + refreshMaterial = true; // set to true on material change + refreshLights = true; // remains set until update done + } + + // load material specific uniforms + // (shader material also gets them for the sake of genericity) + + if ( + material.isShaderMaterial || + material.isMeshPhongMaterial || + material.isMeshStandardMaterial || + material.envMap + ) { + var uCamPos = p_uniforms.map.cameraPosition; + + if (uCamPos !== undefined) { + uCamPos.setValue( + _gl, + _vector3.setFromMatrixPosition(camera.matrixWorld) + ); + } + } + + if ( + material.isMeshPhongMaterial || + material.isMeshLambertMaterial || + material.isMeshBasicMaterial || + material.isMeshStandardMaterial || + material.isShaderMaterial || + material.skinning + ) { + p_uniforms.setValue(_gl, "viewMatrix", camera.matrixWorldInverse); + } + } + + // skinning uniforms must be set even if material didn't change + // auto-setting of texture unit for bone texture must go before other textures + // not sure why, but otherwise weird things happen + + if (material.skinning) { + p_uniforms.setOptional(_gl, object, "bindMatrix"); + p_uniforms.setOptional(_gl, object, "bindMatrixInverse"); + + var skeleton = object.skeleton; + + if (skeleton) { + var bones = skeleton.bones; + + if (capabilities.floatVertexTextures) { + if (skeleton.boneTexture === undefined) { + // layout (1 matrix = 4 pixels) + // RGBA RGBA RGBA RGBA (=> column1, column2, column3, column4) + // with 8x8 pixel texture max 16 bones * 4 pixels = (8 * 8) + // 16x16 pixel texture max 64 bones * 4 pixels = (16 * 16) + // 32x32 pixel texture max 256 bones * 4 pixels = (32 * 32) + // 64x64 pixel texture max 1024 bones * 4 pixels = (64 * 64) + + var size = Math.sqrt(bones.length * 4); // 4 pixels needed for 1 matrix + size = _Math.ceilPowerOfTwo(size); + size = Math.max(size, 4); + + var boneMatrices = new Float32Array(size * size * 4); // 4 floats per RGBA pixel + boneMatrices.set(skeleton.boneMatrices); // copy current values + + var boneTexture = new DataTexture( + boneMatrices, + size, + size, + RGBAFormat, + FloatType + ); + boneTexture.needsUpdate = true; + + skeleton.boneMatrices = boneMatrices; + skeleton.boneTexture = boneTexture; + skeleton.boneTextureSize = size; + } + + p_uniforms.setValue( + _gl, + "boneTexture", + skeleton.boneTexture, + textures + ); + p_uniforms.setValue(_gl, "boneTextureSize", skeleton.boneTextureSize); + } else { + p_uniforms.setOptional(_gl, skeleton, "boneMatrices"); + } + } + } + + if (refreshMaterial) { + p_uniforms.setValue( + _gl, + "toneMappingExposure", + _this.toneMappingExposure + ); + p_uniforms.setValue( + _gl, + "toneMappingWhitePoint", + _this.toneMappingWhitePoint + ); + + if (material.lights) { + // the current material requires lighting info + + // note: all lighting uniforms are always set correctly + // they simply reference the renderer's state for their + // values + // + // use the current material's .needsUpdate flags to set + // the GL state when required + + markUniformsLightsNeedsUpdate(m_uniforms, refreshLights); + } + + // refresh uniforms common to several materials + + if (fog && material.fog) { + refreshUniformsFog(m_uniforms, fog); + } + + if (material.isMeshBasicMaterial) { + refreshUniformsCommon(m_uniforms, material); + } else if (material.isMeshLambertMaterial) { + refreshUniformsCommon(m_uniforms, material); + refreshUniformsLambert(m_uniforms, material); + } else if (material.isMeshPhongMaterial) { + refreshUniformsCommon(m_uniforms, material); + + if (material.isMeshToonMaterial) { + refreshUniformsToon(m_uniforms, material); + } else { + refreshUniformsPhong(m_uniforms, material); + } + } else if (material.isMeshStandardMaterial) { + refreshUniformsCommon(m_uniforms, material); + + if (material.isMeshPhysicalMaterial) { + refreshUniformsPhysical(m_uniforms, material); + } else { + refreshUniformsStandard(m_uniforms, material); + } + } else if (material.isMeshMatcapMaterial) { + refreshUniformsCommon(m_uniforms, material); + + refreshUniformsMatcap(m_uniforms, material); + } else if (material.isMeshDepthMaterial) { + refreshUniformsCommon(m_uniforms, material); + refreshUniformsDepth(m_uniforms, material); + } else if (material.isMeshDistanceMaterial) { + refreshUniformsCommon(m_uniforms, material); + refreshUniformsDistance(m_uniforms, material); + } else if (material.isMeshNormalMaterial) { + refreshUniformsCommon(m_uniforms, material); + refreshUniformsNormal(m_uniforms, material); + } else if (material.isLineBasicMaterial) { + refreshUniformsLine(m_uniforms, material); + + if (material.isLineDashedMaterial) { + refreshUniformsDash(m_uniforms, material); + } + } else if (material.isPointsMaterial) { + refreshUniformsPoints(m_uniforms, material); + } else if (material.isSpriteMaterial) { + refreshUniformsSprites(m_uniforms, material); + } else if (material.isShadowMaterial) { + m_uniforms.color.value.copy(material.color); + m_uniforms.opacity.value = material.opacity; + } + + // RectAreaLight Texture + // TODO (mrdoob): Find a nicer implementation + + if (m_uniforms.ltc_1 !== undefined) + m_uniforms.ltc_1.value = UniformsLib.LTC_1; + if (m_uniforms.ltc_2 !== undefined) + m_uniforms.ltc_2.value = UniformsLib.LTC_2; + + WebGLUniforms.upload( + _gl, + materialProperties.uniformsList, + m_uniforms, + textures + ); + } + + if (material.isShaderMaterial && material.uniformsNeedUpdate === true) { + WebGLUniforms.upload( + _gl, + materialProperties.uniformsList, + m_uniforms, + textures + ); + material.uniformsNeedUpdate = false; + } + + if (material.isSpriteMaterial) { + p_uniforms.setValue(_gl, "center", object.center); + } + + // common matrices + + p_uniforms.setValue(_gl, "modelViewMatrix", object.modelViewMatrix); + p_uniforms.setValue(_gl, "normalMatrix", object.normalMatrix); + p_uniforms.setValue(_gl, "modelMatrix", object.matrixWorld); + + return program; + } + + // Uniforms (refresh uniforms objects) + + function refreshUniformsCommon(uniforms, material) { + uniforms.opacity.value = material.opacity; + + if (material.color) { + uniforms.diffuse.value.copy(material.color); + } + + if (material.emissive) { + uniforms.emissive.value + .copy(material.emissive) + .multiplyScalar(material.emissiveIntensity); + } + + if (material.map) { + uniforms.map.value = material.map; + } + + if (material.alphaMap) { + uniforms.alphaMap.value = material.alphaMap; + } + + if (material.specularMap) { + uniforms.specularMap.value = material.specularMap; + } + + if (material.envMap) { + uniforms.envMap.value = material.envMap; + + // don't flip CubeTexture envMaps, flip everything else: + // WebGLRenderTargetCube will be flipped for backwards compatibility + // WebGLRenderTargetCube.texture will be flipped because it's a Texture and NOT a CubeTexture + // this check must be handled differently, or removed entirely, if WebGLRenderTargetCube uses a CubeTexture in the future + uniforms.flipEnvMap.value = material.envMap.isCubeTexture ? -1 : 1; + + uniforms.reflectivity.value = material.reflectivity; + uniforms.refractionRatio.value = material.refractionRatio; + + uniforms.maxMipLevel.value = properties.get( + material.envMap + ).__maxMipLevel; + } + + if (material.lightMap) { + uniforms.lightMap.value = material.lightMap; + uniforms.lightMapIntensity.value = material.lightMapIntensity; + } + + if (material.aoMap) { + uniforms.aoMap.value = material.aoMap; + uniforms.aoMapIntensity.value = material.aoMapIntensity; + } + + // uv repeat and offset setting priorities + // 1. color map + // 2. specular map + // 3. normal map + // 4. bump map + // 5. alpha map + // 6. emissive map + + var uvScaleMap; + + if (material.map) { + uvScaleMap = material.map; + } else if (material.specularMap) { + uvScaleMap = material.specularMap; + } else if (material.displacementMap) { + uvScaleMap = material.displacementMap; + } else if (material.normalMap) { + uvScaleMap = material.normalMap; + } else if (material.bumpMap) { + uvScaleMap = material.bumpMap; + } else if (material.roughnessMap) { + uvScaleMap = material.roughnessMap; + } else if (material.metalnessMap) { + uvScaleMap = material.metalnessMap; + } else if (material.alphaMap) { + uvScaleMap = material.alphaMap; + } else if (material.emissiveMap) { + uvScaleMap = material.emissiveMap; + } + + if (uvScaleMap !== undefined) { + // backwards compatibility + if (uvScaleMap.isWebGLRenderTarget) { + uvScaleMap = uvScaleMap.texture; + } + + if (uvScaleMap.matrixAutoUpdate === true) { + uvScaleMap.updateMatrix(); + } + + uniforms.uvTransform.value.copy(uvScaleMap.matrix); + } + } + + function refreshUniformsLine(uniforms, material) { + uniforms.diffuse.value.copy(material.color); + uniforms.opacity.value = material.opacity; + } + + function refreshUniformsDash(uniforms, material) { + uniforms.dashSize.value = material.dashSize; + uniforms.totalSize.value = material.dashSize + material.gapSize; + uniforms.scale.value = material.scale; + } + + function refreshUniformsPoints(uniforms, material) { + uniforms.diffuse.value.copy(material.color); + uniforms.opacity.value = material.opacity; + uniforms.size.value = material.size * _pixelRatio; + uniforms.scale.value = _height * 0.5; + + uniforms.map.value = material.map; + + if (material.map !== null) { + if (material.map.matrixAutoUpdate === true) { + material.map.updateMatrix(); + } + + uniforms.uvTransform.value.copy(material.map.matrix); + } + } + + function refreshUniformsSprites(uniforms, material) { + uniforms.diffuse.value.copy(material.color); + uniforms.opacity.value = material.opacity; + uniforms.rotation.value = material.rotation; + uniforms.map.value = material.map; + + if (material.map !== null) { + if (material.map.matrixAutoUpdate === true) { + material.map.updateMatrix(); + } + + uniforms.uvTransform.value.copy(material.map.matrix); + } + } + + function refreshUniformsFog(uniforms, fog) { + uniforms.fogColor.value.copy(fog.color); + + if (fog.isFog) { + uniforms.fogNear.value = fog.near; + uniforms.fogFar.value = fog.far; + } else if (fog.isFogExp2) { + uniforms.fogDensity.value = fog.density; + } + } + + function refreshUniformsLambert(uniforms, material) { + if (material.emissiveMap) { + uniforms.emissiveMap.value = material.emissiveMap; + } + } + + function refreshUniformsPhong(uniforms, material) { + uniforms.specular.value.copy(material.specular); + uniforms.shininess.value = Math.max(material.shininess, 1e-4); // to prevent pow( 0.0, 0.0 ) + + if (material.emissiveMap) { + uniforms.emissiveMap.value = material.emissiveMap; + } + + if (material.bumpMap) { + uniforms.bumpMap.value = material.bumpMap; + uniforms.bumpScale.value = material.bumpScale; + if (material.side === BackSide) uniforms.bumpScale.value *= -1; + } + + if (material.normalMap) { + uniforms.normalMap.value = material.normalMap; + uniforms.normalScale.value.copy(material.normalScale); + if (material.side === BackSide) uniforms.normalScale.value.negate(); + } + + if (material.displacementMap) { + uniforms.displacementMap.value = material.displacementMap; + uniforms.displacementScale.value = material.displacementScale; + uniforms.displacementBias.value = material.displacementBias; + } + } + + function refreshUniformsToon(uniforms, material) { + refreshUniformsPhong(uniforms, material); + + if (material.gradientMap) { + uniforms.gradientMap.value = material.gradientMap; + } + } + + function refreshUniformsStandard(uniforms, material) { + uniforms.roughness.value = material.roughness; + uniforms.metalness.value = material.metalness; + + if (material.roughnessMap) { + uniforms.roughnessMap.value = material.roughnessMap; + } + + if (material.metalnessMap) { + uniforms.metalnessMap.value = material.metalnessMap; + } + + if (material.emissiveMap) { + uniforms.emissiveMap.value = material.emissiveMap; + } + + if (material.bumpMap) { + uniforms.bumpMap.value = material.bumpMap; + uniforms.bumpScale.value = material.bumpScale; + if (material.side === BackSide) uniforms.bumpScale.value *= -1; + } + + if (material.normalMap) { + uniforms.normalMap.value = material.normalMap; + uniforms.normalScale.value.copy(material.normalScale); + if (material.side === BackSide) uniforms.normalScale.value.negate(); + } + + if (material.displacementMap) { + uniforms.displacementMap.value = material.displacementMap; + uniforms.displacementScale.value = material.displacementScale; + uniforms.displacementBias.value = material.displacementBias; + } + + if (material.envMap) { + //uniforms.envMap.value = material.envMap; // part of uniforms common + uniforms.envMapIntensity.value = material.envMapIntensity; + } + } + + function refreshUniformsPhysical(uniforms, material) { + refreshUniformsStandard(uniforms, material); + + uniforms.reflectivity.value = material.reflectivity; // also part of uniforms common + + uniforms.clearcoat.value = material.clearcoat; + uniforms.clearcoatRoughness.value = material.clearcoatRoughness; + if (material.sheen) uniforms.sheen.value.copy(material.sheen); + + if (material.clearcoatNormalMap) { + uniforms.clearcoatNormalScale.value.copy(material.clearcoatNormalScale); + uniforms.clearcoatNormalMap.value = material.clearcoatNormalMap; + + if (material.side === BackSide) { + uniforms.clearcoatNormalScale.value.negate(); + } + } + + uniforms.transparency.value = material.transparency; + } + + function refreshUniformsMatcap(uniforms, material) { + if (material.matcap) { + uniforms.matcap.value = material.matcap; + } + + if (material.bumpMap) { + uniforms.bumpMap.value = material.bumpMap; + uniforms.bumpScale.value = material.bumpScale; + if (material.side === BackSide) uniforms.bumpScale.value *= -1; + } + + if (material.normalMap) { + uniforms.normalMap.value = material.normalMap; + uniforms.normalScale.value.copy(material.normalScale); + if (material.side === BackSide) uniforms.normalScale.value.negate(); + } + + if (material.displacementMap) { + uniforms.displacementMap.value = material.displacementMap; + uniforms.displacementScale.value = material.displacementScale; + uniforms.displacementBias.value = material.displacementBias; + } + } + + function refreshUniformsDepth(uniforms, material) { + if (material.displacementMap) { + uniforms.displacementMap.value = material.displacementMap; + uniforms.displacementScale.value = material.displacementScale; + uniforms.displacementBias.value = material.displacementBias; + } + } + + function refreshUniformsDistance(uniforms, material) { + if (material.displacementMap) { + uniforms.displacementMap.value = material.displacementMap; + uniforms.displacementScale.value = material.displacementScale; + uniforms.displacementBias.value = material.displacementBias; + } + + uniforms.referencePosition.value.copy(material.referencePosition); + uniforms.nearDistance.value = material.nearDistance; + uniforms.farDistance.value = material.farDistance; + } + + function refreshUniformsNormal(uniforms, material) { + if (material.bumpMap) { + uniforms.bumpMap.value = material.bumpMap; + uniforms.bumpScale.value = material.bumpScale; + if (material.side === BackSide) uniforms.bumpScale.value *= -1; + } + + if (material.normalMap) { + uniforms.normalMap.value = material.normalMap; + uniforms.normalScale.value.copy(material.normalScale); + if (material.side === BackSide) uniforms.normalScale.value.negate(); + } + + if (material.displacementMap) { + uniforms.displacementMap.value = material.displacementMap; + uniforms.displacementScale.value = material.displacementScale; + uniforms.displacementBias.value = material.displacementBias; + } + } + + // If uniforms are marked as clean, they don't need to be loaded to the GPU. + + function markUniformsLightsNeedsUpdate(uniforms, value) { + uniforms.ambientLightColor.needsUpdate = value; + uniforms.lightProbe.needsUpdate = value; + + uniforms.directionalLights.needsUpdate = value; + uniforms.pointLights.needsUpdate = value; + uniforms.spotLights.needsUpdate = value; + uniforms.rectAreaLights.needsUpdate = value; + uniforms.hemisphereLights.needsUpdate = value; + } + + // + this.setFramebuffer = function (value) { + if (_framebuffer !== value) _gl.bindFramebuffer(_gl.FRAMEBUFFER, value); + + _framebuffer = value; + }; + + this.getActiveCubeFace = function () { + return _currentActiveCubeFace; + }; + + this.getActiveMipmapLevel = function () { + return _currentActiveMipmapLevel; + }; + + this.getRenderTarget = function () { + return _currentRenderTarget; + }; + + this.setRenderTarget = function ( + renderTarget, + activeCubeFace, + activeMipmapLevel + ) { + _currentRenderTarget = renderTarget; + _currentActiveCubeFace = activeCubeFace; + _currentActiveMipmapLevel = activeMipmapLevel; + + if ( + renderTarget && + properties.get(renderTarget).__webglFramebuffer === undefined + ) { + textures.setupRenderTarget(renderTarget); + } + + var framebuffer = _framebuffer; + var isCube = false; + + if (renderTarget) { + var __webglFramebuffer = properties.get(renderTarget).__webglFramebuffer; + + if (renderTarget.isWebGLRenderTargetCube) { + framebuffer = __webglFramebuffer[activeCubeFace || 0]; + isCube = true; + } else if (renderTarget.isWebGLMultisampleRenderTarget) { + framebuffer = + properties.get(renderTarget).__webglMultisampledFramebuffer; + } else { + framebuffer = __webglFramebuffer; + } + + _currentViewport.copy(renderTarget.viewport); + _currentScissor.copy(renderTarget.scissor); + _currentScissorTest = renderTarget.scissorTest; + } else { + _currentViewport.copy(_viewport).multiplyScalar(_pixelRatio).floor(); + _currentScissor.copy(_scissor).multiplyScalar(_pixelRatio).floor(); + _currentScissorTest = _scissorTest; + } + + if (_currentFramebuffer !== framebuffer) { + _gl.bindFramebuffer(_gl.FRAMEBUFFER, framebuffer); + _currentFramebuffer = framebuffer; + } + + state.viewport(_currentViewport); + state.scissor(_currentScissor); + state.setScissorTest(_currentScissorTest); + + if (isCube) { + var textureProperties = properties.get(renderTarget.texture); + _gl.framebufferTexture2D( + _gl.FRAMEBUFFER, + _gl.COLOR_ATTACHMENT0, + _gl.TEXTURE_CUBE_MAP_POSITIVE_X + (activeCubeFace || 0), + textureProperties.__webglTexture, + activeMipmapLevel || 0 + ); + } + }; + + this.readRenderTargetPixels = function ( + renderTarget, + x, + y, + width, + height, + buffer, + activeCubeFaceIndex + ) { + if (!(renderTarget && renderTarget.isWebGLRenderTarget)) { + console.error( + "THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget." + ); + return; + } + + var framebuffer = properties.get(renderTarget).__webglFramebuffer; + + if ( + renderTarget.isWebGLRenderTargetCube && + activeCubeFaceIndex !== undefined + ) { + framebuffer = framebuffer[activeCubeFaceIndex]; + } + + if (framebuffer) { + var restore = false; + + if (framebuffer !== _currentFramebuffer) { + _gl.bindFramebuffer(_gl.FRAMEBUFFER, framebuffer); + + restore = true; + } + + try { + var texture = renderTarget.texture; + var textureFormat = texture.format; + var textureType = texture.type; + + if ( + textureFormat !== RGBAFormat && + utils.convert(textureFormat) !== + _gl.getParameter(_gl.IMPLEMENTATION_COLOR_READ_FORMAT) + ) { + console.error( + "THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format." + ); + return; + } + + if ( + textureType !== UnsignedByteType && + utils.convert(textureType) !== + _gl.getParameter(_gl.IMPLEMENTATION_COLOR_READ_TYPE) && // IE11, Edge and Chrome Mac < 52 (#9513) + !( + textureType === FloatType && + (capabilities.isWebGL2 || + extensions.get("OES_texture_float") || + extensions.get("WEBGL_color_buffer_float")) + ) && // Chrome Mac >= 52 and Firefox + !( + textureType === HalfFloatType && + (capabilities.isWebGL2 + ? extensions.get("EXT_color_buffer_float") + : extensions.get("EXT_color_buffer_half_float")) + ) + ) { + console.error( + "THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type." + ); + return; + } + + if ( + _gl.checkFramebufferStatus(_gl.FRAMEBUFFER) === + _gl.FRAMEBUFFER_COMPLETE + ) { + // the following if statement ensures valid read requests (no out-of-bounds pixels, see #8604) + + if ( + x >= 0 && + x <= renderTarget.width - width && + y >= 0 && + y <= renderTarget.height - height + ) { + _gl.readPixels( + x, + y, + width, + height, + utils.convert(textureFormat), + utils.convert(textureType), + buffer + ); + } + } else { + console.error( + "THREE.WebGLRenderer.readRenderTargetPixels: readPixels from renderTarget failed. Framebuffer not complete." + ); + } + } finally { + if (restore) { + _gl.bindFramebuffer(_gl.FRAMEBUFFER, _currentFramebuffer); + } + } + } + }; + + this.copyFramebufferToTexture = function (position, texture, level) { + var width = texture.image.width; + var height = texture.image.height; + var glFormat = utils.convert(texture.format); + + textures.setTexture2D(texture, 0); + + _gl.copyTexImage2D( + _gl.TEXTURE_2D, + level || 0, + glFormat, + position.x, + position.y, + width, + height, + 0 + ); + }; + + this.copyTextureToTexture = function ( + position, + srcTexture, + dstTexture, + level + ) { + var width = srcTexture.image.width; + var height = srcTexture.image.height; + var glFormat = utils.convert(dstTexture.format); + var glType = utils.convert(dstTexture.type); + + textures.setTexture2D(dstTexture, 0); + + if (srcTexture.isDataTexture) { + _gl.texSubImage2D( + _gl.TEXTURE_2D, + level || 0, + position.x, + position.y, + width, + height, + glFormat, + glType, + srcTexture.image.data + ); + } else { + _gl.texSubImage2D( + _gl.TEXTURE_2D, + level || 0, + position.x, + position.y, + glFormat, + glType, + srcTexture.image + ); + } + }; + + if (typeof __THREE_DEVTOOLS__ !== "undefined") { + __THREE_DEVTOOLS__.dispatchEvent( + new CustomEvent("observe", { detail: this }) + ); // eslint-disable-line no-undef + } +} + +export { WebGLRenderer }; diff --git a/src/test/fixtures/label-continue-break-bug.js b/src/test/fixtures/label-continue-break-bug.js new file mode 100644 index 000000000..12e46cca3 --- /dev/null +++ b/src/test/fixtures/label-continue-break-bug.js @@ -0,0 +1,125 @@ +Object.assign(Interpolant.prototype, { + evaluate: function (t) { + var pp = this.parameterPositions, + i1 = this._cachedIndex, + t1 = pp[i1], + t0 = pp[i1 - 1]; + + validate_interval: { + seek: { + var right; + + linear_scan: { + //- See http://jsperf.com/comparison-to-undefined/3 + //- slower code: + //- + //- if ( t >= t1 || t1 === undefined ) { + forward_scan: if (!(t < t1)) { + for (var giveUpAt = i1 + 2; ; ) { + if (t1 === undefined) { + if (t < t0) break forward_scan; + + // after end + + i1 = pp.length; + this._cachedIndex = i1; + return this.afterEnd_(i1 - 1, t, t0); + } + + if (i1 === giveUpAt) break; // this loop + + t0 = t1; + t1 = pp[++i1]; + + if (t < t1) { + // we have arrived at the sought interval + break seek; + } + } + + // prepare binary search on the right side of the index + right = pp.length; + break linear_scan; + } + + //- slower code: + //- if ( t < t0 || t0 === undefined ) { + if (!(t >= t0)) { + // looping? + + var t1global = pp[1]; + + if (t < t1global) { + i1 = 2; // + 1, using the scan for the details + t0 = t1global; + } + + // linear reverse scan + + for (var giveUpAt = i1 - 2; ; ) { + if (t0 === undefined) { + // before start + + this._cachedIndex = 0; + return this.beforeStart_(0, t, t1); + } + + if (i1 === giveUpAt) break; // this loop + + t1 = t0; + t0 = pp[--i1 - 1]; + + if (t >= t0) { + // we have arrived at the sought interval + break seek; + } + } + + // prepare binary search on the left side of the index + right = i1; + i1 = 0; + break linear_scan; + } + + // the interval is valid + + break validate_interval; + } // linear scan + + // binary search + + while (i1 < right) { + var mid = (i1 + right) >>> 1; + + if (t < pp[mid]) { + right = mid; + } else { + i1 = mid + 1; + } + } + + t1 = pp[i1]; + t0 = pp[i1 - 1]; + + // check boundary cases, again + + if (t0 === undefined) { + this._cachedIndex = 0; + return this.beforeStart_(0, t, t1); + } + + if (t1 === undefined) { + i1 = pp.length; + this._cachedIndex = i1; + return this.afterEnd_(i1 - 1, t0, t); + } + } // seek + + this._cachedIndex = i1; + + this.intervalChanged_(i1, t0, t1); + } // validate_interval + + return this.interpolate_(i1, t0, t, t1); + }, +}); diff --git a/src/test/fixtures/symbols-bug.js b/src/test/fixtures/symbols-bug.js new file mode 100644 index 000000000..f40ec8c50 --- /dev/null +++ b/src/test/fixtures/symbols-bug.js @@ -0,0 +1,16 @@ +var boom = { + a: 2, + b: "4", + c: "6", + d: "8", + e: "10", + f: 12, + g: 14, +}["15"]; + +const foo = "bacon"; +const james = "not-bacon"; +const lordy = "sammy"; +const boop = { + hey: { foo }, +}; diff --git a/src/test/tester.zig b/src/test/tester.zig index 6b3174eac..81e1c8ea2 100644 --- a/src/test/tester.zig +++ b/src/test/tester.zig @@ -66,6 +66,10 @@ pub const Tester = struct { } pub fn evaluate_outcome(self: *const @This()) Outcome { + if (self.expected.len > self.result.len) { + return .fail; + } + for (self.expected) |char, i| { if (char != self.result[i]) { return Outcome.fail; |