From 107310d785ee9dfbd258a1fc015976a76cdcef82 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 30 Apr 2021 15:34:31 -0700 Subject: [PATCH] inching closure --- .vscode/launch.json | 34 +-- README.md | 2 + src/js_ast.zig | 15 +- src/js_lexer.zig | 35 ++- src/js_parser.zig | 550 +++++++++++++++++++++++++++++++++++---- src/logger.zig | 9 + src/main.zig | 13 +- src/string_immutable.zig | 49 ++++ 8 files changed, 605 insertions(+), 102 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 82c399869e..e1661ff885 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,38 +1,6 @@ { "version": "0.2.0", "configurations": [ - { - "name": "(lldb) Launch", - "type": "cppdbg", - "request": "launch", - "program": "/Users/jarredsumner/Code/esdev/src/zig-cache/o/b57013855157d9a38baa6327511eaf3e/test", - "cwd": "${workspaceFolder}", - "args": ["/Users/jarredsumner/Builds/zig/build/bin/zig"], - "stopAtEntry": false, - "environment": [], - "miDebuggerPath": "/usr/local/bin/lldb-mi", - "MIMode": "lldb", - "targetArchitecture": "x64", - - "externalConsole": false - }, - - { - "name": "Launch", - "type": "cppdbg", - "request": "launch", - "program": "${workspaceFolder}/zig-cache/bin/esdev", - "args": ["/Users/jarredsumner/Code/devserverless/build.js"], - "stopAtEntry": false, - "cwd": "${workspaceFolder}", - "environment": [], - "externalConsole": false, - // "preLaunchTask": "build", - "MIMode": "lldb", - "internalConsoleOptions": "openOnSessionStart", - "logging": { - "moduleLoad": false - } - } + ] } diff --git a/README.md b/README.md index 12c0c17421..f7ace5b53a 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,8 @@ import { map } from "lodash-es"; const foo = map(["bar", "baz"], (item) => {}); ``` +If + ##### HMR & Fast Refresh implementation This section only applies when Hot Module Reloading is enabled. When it's off, none of this part runs. React Fast Refresh depends on Hot Module Reloading. diff --git a/src/js_ast.zig b/src/js_ast.zig index a25c0f30a9..46cf199227 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -2713,8 +2713,8 @@ pub const Ast = struct { // is conveniently fully parallelized. named_imports: std.AutoHashMap(Ref, NamedImport) = undefined, named_exports: std.StringHashMap(NamedExport) = undefined, - top_level_symbol_to_parts: std.AutoHashMap(Ref, []u32) = undefined, - export_star_import_records: std.ArrayList(u32) = undefined, + top_level_symbol_to_parts: std.AutoHashMap(Ref, std.ArrayList(u32)) = undefined, + export_star_import_records: []u32 = &([_]u32{}), pub fn initTest(parts: []Part) Ast { return Ast{ @@ -2855,20 +2855,20 @@ pub const Part = struct { scopes: []*Scope = &([_]*Scope{}), // Each is an index into the file-level import record list - import_record_indices: std.ArrayList(u32) = undefined, + import_record_indices: []u32 = &([_]u32{}), // All symbols that are declared in this part. Note that a given symbol may // have multiple declarations, and so may end up being declared in multiple // parts (e.g. multiple "var" declarations with the same name). Also note // that this list isn't deduplicated and may contain duplicates. - declared_symbols: std.ArrayList(DeclaredSymbol) = undefined, + declared_symbols: []DeclaredSymbol = &([_]DeclaredSymbol{}), // An estimate of the number of uses of all symbols used within this part. - symbol_uses: std.AutoHashMap(Ref, Symbol.Use) = undefined, + symbol_uses: SymbolUseMap = undefined, // The indices of the other parts in this file that are needed if this part // is needed. - dependencies: std.ArrayList(Dependency) = undefined, + dependencies: []Dependency = &([_]Dependency{}), // If true, this part can be removed if none of the declared symbols are // used. If the file containing this part is imported, then all parts that @@ -2883,6 +2883,7 @@ pub const Part = struct { // This is true if this file has been marked as live by the tree shaking // algorithm. is_live: bool = false, + pub const SymbolUseMap = std.AutoHashMap(Ref, Symbol.Use); }; pub const Result = struct { @@ -2897,7 +2898,7 @@ pub const StmtOrExpr = union(enum) { pub const NamedImport = struct { // Parts within this file that use this import - local_parts_with_uses: ?[]u32, + local_parts_with_uses: []u32 = &([_]u32{}), alias: ?string, alias_loc: ?logger.Loc, diff --git a/src/js_lexer.zig b/src/js_lexer.zig index bf2f6cef2e..d618500572 100644 --- a/src/js_lexer.zig +++ b/src/js_lexer.zig @@ -21,6 +21,8 @@ pub const PropertyModifierKeyword = tables.PropertyModifierKeyword; pub const TypescriptStmtKeyword = tables.TypescriptStmtKeyword; pub const TypeScriptAccessibilityModifier = tables.TypeScriptAccessibilityModifier; +pub var emptyJavaScriptString = ([_]u16{0}); + pub const JSONOptions = struct { allow_comments: bool = false, allow_trailing_commas: bool = false, @@ -906,12 +908,13 @@ pub const Lexer = struct { } pub fn unexpected(lexer: *@This()) void { - var found: string = undefined; - if (lexer.start == lexer.source.contents.len) { - found = "end of file"; - } else { - found = lexer.raw(); - } + const found = finder: { + if (lexer.start == lexer.source.contents.len) { + break :finder "end of file"; + } else { + break :finder lexer.raw(); + } + }; lexer.addRangeError(lexer.range(), "Unexpected {s}", .{found}, true); } @@ -925,10 +928,14 @@ pub const Lexer = struct { } pub fn expectedString(self: *@This(), text: string) void { - var found = self.raw(); - if (self.source.contents.len == self.start) { - found = "end of file"; - } + const found = finder: { + if (self.source.contents.len != self.start) { + break :finder self.raw(); + } else { + break :finder "end of file"; + } + }; + self.addRangeError(self.range(), "Expected {s} but found {s}", .{ text, found }, true); } @@ -969,7 +976,7 @@ pub const Lexer = struct { } pub fn initGlobalName(log: *logger.Log, source: *logger.Source, allocator: *std.mem.Allocator) !@This() { - var empty_string_literal: JavascriptString = undefined; + var empty_string_literal: JavascriptString = emptyJavaScriptString; var lex = @This(){ .log = log, .source = source.*, @@ -986,7 +993,7 @@ pub const Lexer = struct { } pub fn initTSConfig(log: *logger.Log, source: *logger.Source, allocator: *std.mem.Allocator) !@This() { - var empty_string_literal: JavascriptString = undefined; + var empty_string_literal: JavascriptString = emptyJavaScriptString; var lex = @This(){ .log = log, .source = source.*, @@ -1006,7 +1013,7 @@ pub const Lexer = struct { } pub fn initJSON(log: *logger.Log, source: *logger.Source, allocator: *std.mem.Allocator) !@This() { - var empty_string_literal: JavascriptString = undefined; + var empty_string_literal: JavascriptString = &emptyJavaScriptString; var lex = @This(){ .log = log, .source = source.*, @@ -1026,7 +1033,7 @@ pub const Lexer = struct { } pub fn init(log: *logger.Log, source: *logger.Source, allocator: *std.mem.Allocator) !@This() { - var empty_string_literal: JavascriptString = undefined; + var empty_string_literal: JavascriptString = &emptyJavaScriptString; var lex = @This(){ .log = log, .source = source.*, diff --git a/src/js_parser.zig b/src/js_parser.zig index f15369c6a5..d30813a03a 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -53,16 +53,16 @@ pub const ImportScanner = struct { stmts: []Stmt = &([_]Stmt{}), kept_import_equals: bool = false, removed_import_equals: bool = false, - pub fn scan(p: *P, _stmts: []Stmt) ImportScanner { + pub fn scan(p: *P, stmts: []Stmt) !ImportScanner { var scanner = ImportScanner{}; - var stmts = StmtList.fromOwnedSlice(p.allocator, _stmts); - var stmts_end: usize = 0; - for (stmts.items) |stmt| { + for (stmts) |_stmt| { + // zls needs the hint, it seems. + const stmt: Stmt = _stmt; switch (stmt.data) { .s_import => |st| { - const record = p.import_records[st.import_record_index]; + var record: ImportRecord = p.import_records.items[st.import_record_index]; // The official TypeScript compiler always removes unused imported // symbols. However, we deliberately deviate from the official @@ -120,7 +120,7 @@ pub const ImportScanner = struct { // user is expecting the output to be as small as possible. So we // should omit unused imports. // - const keep_unused_imports = !p.options.preserve_unused_imports_ts; + const keep_unused_imports = !p.options.trim_unused_imports; // TypeScript always trims unused imports. This is important for // correctness since some imports might be fake (only in the type @@ -131,10 +131,10 @@ pub const ImportScanner = struct { if (st.default_name) |default_name| { found_imports = true; - var symbol = p.symbols.items[default_name.ref.inner_index]; + var symbol = p.symbols.items[default_name.ref.?.inner_index]; // TypeScript has a separate definition of unused - if (p.options.ts and p.ts_use_counts.items[default_name.ref.inner_index] != 0) { + if (p.options.ts and p.ts_use_counts.items[default_name.ref.?.inner_index] != 0) { is_unused_in_typescript = false; } @@ -161,7 +161,7 @@ pub const ImportScanner = struct { var has_any = false; if (p.import_items_for_namespace.get(st.namespace_ref)) |entry| { - if (entry.size() > 0) { + if (entry.count() > 0) { has_any = true; } } @@ -171,19 +171,320 @@ pub const ImportScanner = struct { } } } + + // Remove items if they are unused + if (st.items.len > 0) { + found_imports = false; + var items_end: usize = 0; + var i: usize = 0; + while (i < st.items.len) : (i += 1) { + const item = st.items[i]; + const ref = item.name.ref.?; + const symbol: Symbol = p.symbols.items[ref.inner_index]; + + // TypeScript has a separate definition of unused + if (p.options.ts and p.ts_use_counts.items[ref.inner_index] != 0) { + is_unused_in_typescript = false; + } + + // Remove the symbol if it's never used outside a dead code region + if (symbol.use_count_estimate != 0) { + st.items[items_end] = item; + items_end += 1; + } + } + + // Filter the array by taking a slice + if (items_end == 0 and st.items.len > 0) { + p.allocator.free(st.items); + // zero out the slice + st.items = &([_]js_ast.ClauseItem{}); + } else if (items_end < st.items.len) { + var list = List(js_ast.ClauseItem).fromOwnedSlice(p.allocator, st.items); + list.shrinkAndFree(items_end); + st.items = list.toOwnedSlice(); + } + } + + // -- Original Comment -- + // Omit this statement if we're parsing TypeScript and all imports are + // unused. Note that this is distinct from the case where there were + // no imports at all (e.g. "import 'foo'"). In that case we want to keep + // the statement because the user is clearly trying to import the module + // for side effects. + // + // This culling is important for correctness when parsing TypeScript + // because a) the TypeScript compiler does ths and we want to match it + // and b) this may be a fake module that only exists in the type system + // and doesn't actually exist in reality. + // + // We do not want to do this culling in JavaScript though because the + // module may have side effects even if all imports are unused. + // -- Original Comment -- + + // jarred: I think, in this project, we want this behavior, even in JavaScript. + // I think this would be a big performance improvement. + // The less you import, the less code you transpile. + // Side-effect imports are nearly always done through identifier-less imports + // e.g. `import 'fancy-stylesheet-thing/style.css';` + // This is a breaking change though. We can make it an option with some guardrail + // so maybe if it errors, it shows a suggestion "retry without trimming unused imports" + if (found_imports and !p.options.preserve_unused_imports_ts) { + // Ignore import records with a pre-filled source index. These are + // for injected files and we definitely do not want to trim these. + if (!Ref.isSourceIndexNull(record.source_index)) { + record.is_unused = true; + continue; + } + } + } + + if (p.options.trim_unused_imports) { + if (st.star_name_loc != null) { + // -- Original Comment -- + // If we're bundling a star import and the namespace is only ever + // used for property accesses, then convert each unique property to + // a clause item in the import statement and remove the star import. + // That will cause the bundler to bundle them more efficiently when + // both this module and the imported module are in the same group. + // + // Before: + // + // import * as ns from 'foo' + // console.log(ns.a, ns.b) + // + // After: + // + // import {a, b} from 'foo' + // console.log(a, b) + // + // This is not done if the namespace itself is used, because in that + // case the code for the namespace will have to be generated. This is + // determined by the symbol count because the parser only counts the + // star import as used if it was used for something other than a + // property access: + // + // import * as ns from 'foo' + // console.log(ns, ns.a, ns.b) + // + // -- Original Comment -- + + // jarred: we don't use the same grouping mechanism as esbuild + // but, we do this anyway. + // The reasons why are: + // * It makes static analysis for other tools simpler. + // * I imagine browsers may someday do some optimizations + // when it's "easier" to know only certain modules are used + // For example, if you're importing a component from a design system + // it's really stupid to import all 1,000 components from that design system + // when you just want