diff options
-rw-r--r-- | src/ast/base.zig | 50 | ||||
-rw-r--r-- | src/fs.zig | 72 | ||||
-rw-r--r-- | src/import_record.zig | 4 | ||||
-rw-r--r-- | src/js_ast.zig | 224 | ||||
-rw-r--r-- | src/js_lexer.zig | 49 | ||||
-rw-r--r-- | src/js_parser.zig | 1 | ||||
-rw-r--r-- | src/js_printer.zig | 999 | ||||
-rw-r--r-- | src/json_parser.zig | 4 | ||||
-rw-r--r-- | src/linker.zig | 11 | ||||
-rw-r--r-- | src/logger.zig | 8 | ||||
-rw-r--r-- | src/main.zig | 12 | ||||
-rw-r--r-- | src/options.zig | 5 | ||||
-rw-r--r-- | src/renamer.zig | 2 | ||||
-rw-r--r-- | src/string_immutable.zig | 83 |
14 files changed, 1338 insertions, 186 deletions
diff --git a/src/ast/base.zig b/src/ast/base.zig index 744b7e2d9..f941e3745 100644 --- a/src/ast/base.zig +++ b/src/ast/base.zig @@ -1,4 +1,5 @@ -const unicode = @import("std").unicode; +const std = @import("std"); +const unicode = std.unicode; pub const JavascriptString = []u16; pub fn newJavascriptString(comptime text: []const u8) JavascriptString { @@ -7,3 +8,50 @@ pub fn newJavascriptString(comptime text: []const u8) JavascriptString { pub const NodeIndex = u32; pub const NodeIndexNone = 4294967293; + +// TODO: figure out if we actually need this +// -- original comment -- +// Files are parsed in parallel for speed. We want to allow each parser to +// generate symbol IDs that won't conflict with each other. We also want to be +// able to quickly merge symbol tables from all files into one giant symbol +// table. +// +// We can accomplish both goals by giving each symbol ID two parts: a source +// index that is unique to the parser goroutine, and an inner index that +// increments as the parser generates new symbol IDs. Then a symbol map can +// be an array of arrays indexed first by source index, then by inner index. +// The maps can be merged quickly by creating a single outer array containing +// all inner arrays from all parsed files. +pub const Ref = packed struct { + source_index: Int = std.math.maxInt(Ref.Int), + inner_index: Int = 0, + + // 2 bits of padding for whatever is the parent + pub const Int = u31; + pub const None = Ref{ .inner_index = std.math.maxInt(Ref.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); + } + + pub fn isSourceNull(self: *const Ref) bool { + return self.source_index == std.math.maxInt(Ref.Int); + } + + pub fn isSourceIndexNull(int: Ref.Int) bool { + return int == std.math.maxInt(Ref.Int); + } + + pub fn eql(ref: Ref, b: Ref) bool { + return ref.inner_index == b.inner_index and ref.source_index == b.source_index; + } +}; + +// This is kind of the wrong place, but it's shared between files +pub const RequireOrImportMeta = struct { + // CommonJS files will return the "require_*" wrapper function and an invalid + // exports object reference. Lazily-initialized ESM files will return the + // "init_*" wrapper function and the exports object for that file. + wrapper_ref: Ref = Ref.None, + exports_ref: Ref = Ref.None, + is_wrapper_async: bool = false, +}; diff --git a/src/fs.zig b/src/fs.zig index 4f2918412..82302b697 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -7,19 +7,75 @@ const expect = std.testing.expect; // pub const FilesystemImplementation = @import("fs_impl.zig"); -pub const FileSystem = struct { tree: std.AutoHashMap(FileSystemEntry) }; +// +pub const Stat = packed struct { + // milliseconds + mtime: i64 = 0, + // last queried timestamp + qtime: i64 = 0, + kind: FileSystemEntry.Kind, +}; + +pub const FileSystem = struct { + // This maps paths relative to absolute_working_dir to the structure of arrays of paths + stats: std.StringHashMap(Stat) = undefined, + entries: std.ArrayList(FileSystemEntry), + + absolute_working_dir = "/", + implementation: anytype = undefined, + + // pub fn statBatch(fs: *FileSystemEntry, paths: []string) ![]?Stat { + + // } + // pub fn stat(fs: *FileSystemEntry, path: string) !Stat { + + // } + // pub fn readFile(fs: *FileSystemEntry, path: string) ?string { -pub const FileSystemEntry = union(enum) { + // } + // pub fn readDir(fs: *FileSystemEntry, path: string) ?[]string { + + // } + + pub fn Implementation(comptime Context: type) type { + return struct { + context: *Context, + + pub fn statBatch(context: *Context, path: string) ![]?Stat { + return try context.statBatch(path); + } + + pub fn stat(context: *Context, path: string) !?Stat { + return try context.stat(path); + } + + pub fn readFile(context: *Context, path: string) !?File { + return try context.readFile(path); + } + + pub fn readDir(context: *Context, path: string) []string { + return context.readdir(path); + } + }; + } +}; + +pub const FileNotFound = struct {}; + +pub const FileSystemEntry = union(FileSystemEntry.Kind) { file: File, directory: Directory, -}; + not_found: FileNotFound, -pub const File = struct { - path: Path, - mtime: ?usize, - contents: ?string, + pub const Kind = enum(u8) { + file, + directory, + not_found, + }; }; -pub const Directory = struct { path: Path, mtime: ?usize, contents: []FileSystemEntry }; + +pub const Directory = struct { path: Path, contents: []string }; +pub const File = struct { path: Path, contents: string }; pub const PathName = struct { base: string, diff --git a/src/import_record.zig b/src/import_record.zig index 60621379f..ff1fa2768 100644 --- a/src/import_record.zig +++ b/src/import_record.zig @@ -1,5 +1,7 @@ const fs = @import("fs.zig"); const logger = @import("logger.zig"); +const std = @import("std"); +usingnamespace @import("ast/base.zig"); pub const ImportKind = enum(u8) { @@ -32,6 +34,8 @@ pub const ImportRecord = struct { range: logger.Range, path: fs.Path, + source_index: Ref.Int = std.math.maxInt(Ref.Int), + // Sometimes the parser creates an import record and decides it isn't needed. // For example, TypeScript code may have import statements that later turn // out to be type-only imports after analyzing the whole file. diff --git a/src/js_ast.zig b/src/js_ast.zig index 5a5e6dec4..0cbdc0bda 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -41,39 +41,6 @@ pub const ExprNodeList = []Expr; pub const StmtNodeList = []Stmt; pub const BindingNodeList = []Binding; -// TODO: figure out if we actually need this -// -- original comment -- -// Files are parsed in parallel for speed. We want to allow each parser to -// generate symbol IDs that won't conflict with each other. We also want to be -// able to quickly merge symbol tables from all files into one giant symbol -// table. -// -// We can accomplish both goals by giving each symbol ID two parts: a source -// index that is unique to the parser goroutine, and an inner index that -// increments as the parser generates new symbol IDs. Then a symbol map can -// be an array of arrays indexed first by source index, then by inner index. -// The maps can be merged quickly by creating a single outer array containing -// all inner arrays from all parsed files. -pub const Ref = packed struct { - source_index: Ref.Int = std.math.maxInt(Ref.Int), - inner_index: Ref.Int, - - // 2 bits of padding for whatever is the parent - pub const Int = u31; - const None = Ref{ .inner_index = std.math.maxInt(Ref.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); - } - - pub fn isSourceNull(self: *const Ref) bool { - return self.source_index == std.math.maxInt(Ref.Int); - } - - pub fn eql(ref: *Ref, b: Ref) bool { - return ref.inner_index == b.inner_index and ref.source_index == b.source_index; - } -}; - pub const ImportItemStatus = packed enum { none, @@ -547,7 +514,7 @@ pub const Symbol = struct { symbols_for_source: [][]Symbol = undefined, pub fn get(self: *Map, ref: Ref) ?Symbol { - self.symbols_for_source[ref.source_index][ref.inner_index]; + return self.symbols_for_source[ref.source_index][ref.inner_index]; } pub fn init(sourceCount: usize, allocator: *std.mem.Allocator) !Map { @@ -557,15 +524,12 @@ pub const Symbol = struct { pub fn follow(symbols: *Map, ref: Ref) Ref { if (symbols.get(ref)) |*symbol| { - if (symbol.link) |link| { - if (!link.eql(ref)) { - symbol.link = ref; - } - - return link; - } else { - return symbol; + const link = symbol.link orelse return ref; + if (!link.eql(ref)) { + symbol.link = ref; } + + return symbol.link orelse unreachable; } else { return ref; } @@ -805,7 +769,7 @@ pub const E = struct { tag: ?ExprNodeIndex = null, head: JavascriptString, head_raw: string, // This is only filled out for tagged template literals - parts: ?[]TemplatePart = null, + parts: []TemplatePart = &([_]TemplatePart{}), legacy_octal_loc: logger.Loc = logger.Loc.Empty, }; @@ -829,16 +793,16 @@ pub const E = struct { }; pub const Require = struct { - import_record_index: u32 = 0, + import_record_index: Ref.Int = 0, }; pub const RequireOrRequireResolve = struct { - import_record_index: u32 = 0, + import_record_index: Ref.Int = 0, }; pub const Import = struct { expr: ExprNodeIndex, - import_record_index: u32, + import_record_index: Ref.Int, // Comments inside "import()" expressions have special meaning for Webpack. // Preserving comments inside these expressions makes it possible to use @@ -2109,6 +2073,15 @@ pub const Expr = struct { ); } + pub fn isOptionalChain(self: *const @This()) bool { + return switch (self.data) { + .e_dot => |dot| dot.optional_chain != null, + .e_index => |dot| dot.optional_chain != null, + .e_call => |dot| dot.optional_chain != null, + else => false, + }; + } + pub const Data = union(Tag) { e_array: *E.Array, e_unary: *E.Unary, @@ -2147,15 +2120,6 @@ pub const Expr = struct { e_require_or_require_resolve: *E.RequireOrRequireResolve, e_import: *E.Import, - pub fn isOptionalChain(self: *Expr) bool { - return switch (self) { - Expr.e_dot => |dot| dot.optional_chain != null, - Expr.e_index => |dot| dot.optional_chain != null, - Expr.e_call => |dot| dot.optional_chain != null, - else => false, - }; - } - pub fn isBooleanValue(self: *Expr) bool { // TODO: return false; @@ -2371,7 +2335,7 @@ pub const Finally = struct { pub const Case = struct { loc: logger.Loc, value: ?ExprNodeIndex, body: StmtNodeList }; pub const Op = struct { - // If you add a new token, remember to add it to "OpTable" too + // If you add a new token, remember to add it to "Table" too pub const Code = enum { // Prefix un_pos, @@ -2437,6 +2401,33 @@ pub const Op = struct { bin_nullish_coalescing_assign, bin_logical_or_assign, bin_logical_and_assign, + + pub fn unaryAssignTarget(code: Op.Code) AssignTarget { + if (@enumToInt(code) >= @enumToInt(Op.Code.un_pre_dec) and @enumToInt(code) <= @enumToInt(Op.Code.un_post_inc)) { + return AssignTarget.update; + } else { + return AssignTarget.none; + } + } + pub fn isLeftAssociative(code: Op.Code) bool { + return @enumToInt(code) >= @enumToInt(Op.Code.bin_add) and @enumToInt(code) < @enumToInt(Op.Code.bin_comma) and code != .bin_pow; + } + pub fn isRightAssociative(code: Op.Code) bool { + return @enumToInt(code) >= @enumToInt(Op.Code.bin_assign) or code == .bin_pow; + } + pub fn binaryAssignTarget(code: Op.Code) AssignTarget { + if (code == .bin_assign) { + return AssignTarget.replace; + } else if (@enumToInt(code) > @enumToInt(Op.Code.bin_assign)) { + return .update; + } else { + return .none; + } + } + + pub fn isPrefix(code: Op.Code) bool { + return @enumToInt(code) < @enumToInt(Op.Code.un_post_dec); + } }; pub const Level = packed enum(u6) { @@ -2492,71 +2483,84 @@ pub const Op = struct { level: Level, is_keyword: bool = false, - const Table = []Op{ + pub fn init(triple: anytype) Op { + return Op{ + .text = triple.@"0", + .level = triple.@"1", + .is_keyword = triple.@"2", + }; + } + + pub const TableType: std.EnumArray(Op.Code, Op); + pub const Table = comptime { + var table = std.EnumArray(Op.Code, Op).initUndefined(); + // Prefix - .{ "+", Level.prefix, false }, - .{ "-", Level.prefix, false }, - .{ "~", Level.prefix, false }, - .{ "!", Level.prefix, false }, - .{ "void", Level.prefix, true }, - .{ "typeof", Level.prefix, true }, - .{ "delete", Level.prefix, true }, + table.set(Op.Code.un_pos, Op.init(.{ "+", Level.prefix, false })); + table.set(Op.Code.un_neg, Op.init(.{ "-", Level.prefix, false })); + table.set(Op.Code.un_cpl, Op.init(.{ "~", Level.prefix, false })); + table.set(Op.Code.un_not, Op.init(.{ "!", Level.prefix, false })); + table.set(Op.Code.un_void, Op.init(.{ "void", Level.prefix, true })); + table.set(Op.Code.un_typeof, Op.init(.{ "typeof", Level.prefix, true })); + table.set(Op.Code.un_delete, Op.init(.{ "delete", Level.prefix, true })); // Prefix update - .{ "--", Level.prefix, false }, - .{ "++", Level.prefix, false }, + table.set(Op.Code.un_pre_dec, Op.init(.{ "--", Level.prefix, false })); + table.set(Op.Code.un_pre_inc, Op.init(.{ "++", Level.prefix, false })); // Postfix update - .{ "--", Level.postfix, false }, - .{ "++", Level.postfix, false }, + table.set(Op.Code.un_post_dec, Op.init(.{ "--", Level.postfix, false })); + table.set(Op.Code.un_post_inc, Op.init(.{ "++", Level.postfix, false })); // Left-associative - .{ "+", Level.add, false }, - .{ "-", Level.add, false }, - .{ "*", Level.multiply, false }, - .{ "/", Level.multiply, false }, - .{ "%", Level.multiply, false }, - .{ "**", Level.exponentiation, false }, // Right-associative - .{ "<", Level.compare, false }, - .{ "<=", Level.compare, false }, - .{ ">", Level.compare, false }, - .{ ">=", Level.compare, false }, - .{ "in", Level.compare, true }, - .{ "instanceof", Level.compare, true }, - .{ "<<", Level.shift, false }, - .{ ">>", Level.shift, false }, - .{ ">>>", Level.shift, false }, - .{ "==", Level.equals, false }, - .{ "!=", Level.equals, false }, - .{ "===", Level.equals, false }, - .{ "!==", Level.equals, false }, - .{ "??", Level.nullish_coalescing, false }, - .{ "||", Level.logical_or, false }, - .{ "&&", Level.logical_and, false }, - .{ "|", Level.bitwise_or, false }, - .{ "&", Level.bitwise_and, false }, - .{ "^", Level.bitwise_xor, false }, + table.set(Op.Code.bin_add, Op.init(.{ "+", Level.add, false })); + table.set(Op.Code.bin_sub, Op.init(.{ "-", Level.add, false })); + table.set(Op.Code.bin_mul, Op.init(.{ "*", Level.multiply, false })); + table.set(Op.Code.bin_div, Op.init(.{ "/", Level.multiply, false })); + table.set(Op.Code.bin_rem, Op.init(.{ "%", Level.multiply, false })); + table.set(Op.Code.bin_pow, Op.init(.{ "**", Level.exponentiation, false })); + table.set(Op.Code.bin_lt, Op.init(.{ "<", Level.compare, false })); + table.set(Op.Code.bin_le, Op.init(.{ "<=", Level.compare, false })); + table.set(Op.Code.bin_gt, Op.init(.{ ">", Level.compare, false })); + table.set(Op.Code.bin_ge, Op.init(.{ ">=", Level.compare, false })); + table.set(Op.Code.bin_in, Op.init(.{ "in", Level.compare, true })); + table.set(Op.Code.bin_instanceof, Op.init(.{ "instanceof", Level.compare, true })); + table.set(Op.Code.bin_shl, Op.init(.{ "<<", Level.shift, false })); + table.set(Op.Code.bin_shr, Op.init(.{ ">>", Level.shift, false })); + table.set(Op.Code.bin_u_shr, Op.init(.{ ">>>", Level.shift, false })); + table.set(Op.Code.bin_loose_eq, Op.init(.{ "==", Level.equals, false })); + table.set(Op.Code.bin_loose_ne, Op.init(.{ "!=", Level.equals, false })); + table.set(Op.Code.bin_strict_eq, Op.init(.{ "===", Level.equals, false })); + table.set(Op.Code.bin_strict_ne, Op.init(.{ "!==", Level.equals, false })); + table.set(Op.Code.bin_nullish_coalescing, Op.init(.{ "??", Level.nullish_coalescing, false })); + table.set(Op.Code.bin_logical_or, Op.init(.{ "||", Level.logical_or, false })); + table.set(Op.Code.bin_logical_and, Op.init(.{ "&&", Level.logical_and, false })); + table.set(Op.Code.bin_bitwise_or, Op.init(.{ "|", Level.bitwise_or, false })); + table.set(Op.Code.bin_bitwise_and, Op.init(.{ "&", Level.bitwise_and, false })); + table.set(Op.Code.bin_bitwise_xor, Op.init(.{ "^", Level.bitwise_xor, false })); // Non-associative - .{ ",", LComma, false }, + table.set(Op.Code.bin_comma, Op.init(.{ ",", Level.comma, false })); // Right-associative - .{ "=", Level.assign, false }, - .{ "+=", Level.assign, false }, - .{ "-=", Level.assign, false }, - .{ "*=", Level.assign, false }, - .{ "/=", Level.assign, false }, - .{ "%=", Level.assign, false }, - .{ "**=", Level.assign, false }, - .{ "<<=", Level.assign, false }, - .{ ">>=", Level.assign, false }, - .{ ">>>=", Level.assign, false }, - .{ "|=", Level.assign, false }, - .{ "&=", Level.assign, false }, - .{ "^=", Level.assign, false }, - .{ "??=", Level.assign, false }, - .{ "||=", Level.assign, false }, - .{ "&&=", Level.assign, false }, + table.set(Op.Code.bin_assign, Op.init(.{ "=", Level.assign, false })); + table.set(Op.Code.bin_add_assign, Op.init(.{ "+=", Level.assign, false })); + table.set(Op.Code.bin_sub_assign, Op.init(.{ "-=", Level.assign, false })); + table.set(Op.Code.bin_mul_assign, Op.init(.{ "*=", Level.assign, false })); + table.set(Op.Code.bin_div_assign, Op.init(.{ "/=", Level.assign, false })); + table.set(Op.Code.bin_rem_assign, Op.init(.{ "%=", Level.assign, false })); + table.set(Op.Code.bin_pow_assign, Op.init(.{ "**=", Level.assign, false })); + table.set(Op.Code.bin_shl_assign, Op.init(.{ "<<=", Level.assign, false })); + table.set(Op.Code.bin_shr_assign, Op.init(.{ ">>=", Level.assign, false })); + table.set(Op.Code.bin_u_shr_assign, Op.init(.{ ">>>=", Level.assign, false })); + table.set(Op.Code.bin_bitwise_or_assign, Op.init(.{ "|=", Level.assign, false })); + table.set(Op.Code.bin_bitwise_and_assign, Op.init(.{ "&=", Level.assign, false })); + table.set(Op.Code.bin_bitwise_xor_assign, Op.init(.{ "^=", Level.assign, false })); + table.set(Op.Code.bin_nullish_coalescing_assign, Op.init(.{ "??=", Level.assign, false })); + table.set(Op.Code.bin_logical_or_assign, Op.init(.{ "||=", Level.assign, false })); + table.set(Op.Code.bin_logical_and_assign, Op.init(.{ "&&=", Level.assign, false })); + + return table; }; }; diff --git a/src/js_lexer.zig b/src/js_lexer.zig index bef9893c8..409b3a269 100644 --- a/src/js_lexer.zig +++ b/src/js_lexer.zig @@ -1503,6 +1503,55 @@ pub fn isWhitespace(codepoint: CodePoint) bool { } } +pub fn isIdentifier(text: string) bool { + if (text.len == 0) { + return false; + } + + var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 }; + if (!isIdentifierStart(iter.nextCodepoint() orelse unreachable)) { + return false; + } + + while (iter.nextCodepoint()) |codepoint| { + if (!isIdentifierContinue(@intCast(CodePoint, codepoint))) { + return false; + } + } + + return true; +} + +pub fn isIdentifierUTF16(text: JavascriptString) bool { + const n = text.len; + if (n == 0) { + return false; + } + + var i: usize = 0; + while (i < n) : (i += 1) { + var r1 = text[i]; + if (r1 >= 0xD800 and r1 <= 0xDBFF and i + 1 < n) { + const r2 = text[i + 1]; + if (r2 >= 0xDC00 and r2 <= 0xDFFF) { + r1 = (r1 << 10) + r2 + (0x10000 - (0xD800 << 10) - 0xDC00); + i += 1; + } + } + if (i == 0) { + if (!isIdentifierStart(@intCast(u21, r1))) { + return false; + } + } else { + if (!isIDentifierContinue(@intCast(u21, r1))) { + return false; + } + } + } + + return true; +} + // TODO: implement this to actually work right // this fn is a stub! pub fn rangeOfIdentifier(source: *Source, loc: logger.Loc) logger.Range { diff --git a/src/js_parser.zig b/src/js_parser.zig index 44cb5e036..7fbb83065 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -21,7 +21,6 @@ const StmtNodeList = js_ast.StmtNodeList; const BindingNodeList = js_ast.BindingNodeList; const assert = std.debug.assert; -const Ref = js_ast.Ref; const LocRef = js_ast.LocRef; const S = js_ast.S; const B = js_ast.B; diff --git a/src/js_printer.zig b/src/js_printer.zig index c0f2d3c84..2bd2f5da0 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -16,7 +16,6 @@ const expect = std.testing.expect; const ImportKind = importRecord.ImportKind; const BindingNodeIndex = js_ast.BindingNodeIndex; -const Ref = js_ast.Ref; const LocRef = js_ast.LocRef; const S = js_ast.S; const B = js_ast.B; @@ -40,6 +39,8 @@ const first_high_surrogate: u21 = 0xD800; const last_high_surrogate: u21 = 0xDBFF; const first_low_surrogate: u21 = 0xDC00; const last_low_surrogate: u21 = 0xDFFF; +const assert = std.debug.assert; +const Linker = @import("linker.zig").Linker; fn notimpl() void { std.debug.panic("Not implemented yet!", .{}); @@ -71,6 +72,8 @@ pub const SourceMapChunk = struct { pub const Options = struct { to_module_ref: js_ast.Ref, indent: usize = 0, + + rewrite_require_resolve: bool = true, // If we're writing out a source map, this table of line start indices lets // us do binary search on to figure out what line a given AST node came from // line_offset_tables: []LineOffsetTable @@ -78,11 +81,33 @@ pub const Options = struct { pub const PrintResult = struct { js: string, source_map: ?SourceMapChunk = null }; -const ExprFlag = enum { - forbid_call, - forbid_in, - has_non_optional_chain_parent, - expr_result_is_unused, +// Zig represents booleans in packed structs as 1 bit, with no padding +// This is effectively a bit field +const ExprFlag = packed struct { + forbid_call: bool = false, + forbid_in: bool = false, + has_non_optional_chain_parent: bool = false, + expr_result_is_unused: bool = false, + + pub fn None() ExprFlag { + return ExprFlag{}; + } + + pub fn ForbidCall() ExprFlag { + return ExprFlag{ .forbid_call = true }; + } + + pub fn ForbidAnd() ExprFlag { + return ExprFlag{ .forbid_and = true }; + } + + pub fn HasNonOptionalChainParent() ExprFlag { + return ExprFlag{ .has_non_optional_chain_parent = true }; + } + + pub fn ExprResultIsUnused() ExprFlag { + return ExprFlag{ .expr_result_is_unused = true }; + } }; pub fn NewPrinter(comptime ascii_only: bool) type { @@ -93,7 +118,7 @@ pub fn NewPrinter(comptime ascii_only: bool) type { return struct { symbols: Symbol.Map, import_records: []importRecord.ImportRecord, - + linker: *Linker, js: MutableString, needs_semicolon: bool = false, @@ -110,6 +135,7 @@ pub fn NewPrinter(comptime ascii_only: bool) type { int_to_bytes_buffer: [64]u8 = [_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, writer: MutableString.Writer, allocator: *std.mem.Allocator, + renamer: rename.Renamer, const Printer = @This(); pub fn comptime_flush(p: *Printer) void {} @@ -159,6 +185,10 @@ pub fn NewPrinter(comptime ascii_only: bool) type { // } // } + pub fn printFmt(p: *Printer, fmt: comptime string, args: anytype) void { + std.fmt.bufPrint(p.writer, fmt, args); + } + pub fn print(p: *Printer, str: anytype) void { switch (@TypeOf(str)) { comptime_int => { @@ -204,7 +234,7 @@ pub fn NewPrinter(comptime ascii_only: bool) type { p.print(" "); } pub fn printNewline(p: *Printer) void { - notimpl(); + p.print("\n"); } pub fn printSemicolonAfterStatement(p: *Printer) void { p.print(";\n"); @@ -221,20 +251,25 @@ pub fn NewPrinter(comptime ascii_only: bool) type { } } pub fn printDotThenPrefix(p: *Printer) Level { - return .lowest; + p.print(".then(() => "); + return .comma; } - pub fn printUndefined(level: Level) void { - notimpl(); + pub fn printUndefined(p: *Printer, level: Level) void { + // void 0 is more efficient in output size + // however, "void 0" is the same as "undefined" is a point of confusion for many + // since we are optimizing for development, undefined is more clear. + // an ideal development bundler would output very readable code, even without source maps. + p.print("undefined"); } - pub fn printBody(stmt: Stmt) void { + pub fn printBody(p: *Printer, stmt: Stmt) void { notimpl(); } - pub fn printBlock(loc: logger.Loc, stmts: []Stmt) void { + pub fn printBlock(p: *Printer, loc: logger.Loc, stmts: []Stmt) void { notimpl(); } - pub fn printDecls(keyword: string, decls: []G.Decl, flags: ExprFlag) void { + pub fn printDecls(p: *Printer, keyword: string, decls: []G.Decl, flags: ExprFlag) void { notimpl(); } @@ -254,7 +289,7 @@ pub fn NewPrinter(comptime ascii_only: bool) type { notimpl(); } - pub fn bestQuoteCharForString(p: *Printer, str: JavascriptString, allow_backtick: bool) u8 { + pub fn bestQuoteCharForString(p: *Printer, str: anytype, allow_backtick: bool) u8 { var single_cost: usize = 0; var double_cost: usize = 0; var backtick_cost: usize = 0; @@ -473,72 +508,606 @@ pub fn NewPrinter(comptime ascii_only: bool) type { } } - pub fn printExpr(p: *Printer, expr: Expr, level: Level, flags: ExprFlag) void { + pub fn isUnboundEvalIdentifier(p: *Printer, value: Expr) bool { + switch (value.data) { + .e_identifier => |ident| { + const symbol = p.symbols.get(p.symbols.follow(ident.ref)) orelse return false; + return symbol.kind == .unbound and strings.eql(symbol.original_name, "eval"); + }, + else => { + return false; + }, + } + } + + pub fn printRequireOrImportExpr(p: *Printer, import_record_index: Ref.Int, leading_interior_comments: []G.Comment, _level: Level, flags: ExprFlag) void { + var level = _level; + assert(p.import_records.len > import_record_index); + const record = p.import_records[import_record_index]; + + if (level.gte(.new) or flags.forbid_call) { + p.print("("); + defer p.print(")"); + level = .lowest; + } + + if (Ref.isSourceIndexNull(record.source_index)) { + // External "require()" + // This case should ideally not happen. + // Emitting "require" when targeting a browser is broken code and a sign of something wrong. + if (record.kind != .dynamic) { + + // First, we will assert to make detecting this case a little clearer for us in development. + if (std.builtin.mode == std.builtin.Mode.Debug) { + std.debug.panic("Internal error: {s} is an external require, which should never happen.", .{record}); + } + + p.printSpaceBeforeIdentifier(); + + // Then, we will *dangerously* import it as esm, assuming **top-level await support**. + // This is not a transform that will always work, but it most closely mimicks the behavior of require() + // For ESM interop, webpack & other bundlers typically make the default export the equivalent of require("foo").default + // so that's require("foo").default.bar rather than require("foo").bar + // We are assuming that the target import has been converted into something with an "export default". + // If it's not esm, the code won't work anyway + p.printFmt("/* require(\"{s}\") */(await import(", .{record.path.text}); + p.addSourceMapping(record.range.loc); + p.printQuotedUTF8(record.path.text, true); + p.print(").default)"); + return; + } + + // External import() + if (leading_interior_comments.len > 0) { + p.printNewline(); + p.options.indent += 1; + for (leading_interior_comments) |comment| { + p.printIndentedComment(comment.text); + } + p.printIndent(); + } + p.addSourceMapping(record.range.loc); + p.printQuotedUTF8(record.path.text, true); + if (leading_interior_comments.len > 0) { + p.printNewline(); + p.options.indent -= 1; + p.printIndent(); + } + + return; + } + + var meta = p.linker.requireOrImportMetaForSource(record.source_index); + + // Don't need the namespace object if the result is unused anyway + if (flags.expr_result_is_unused) { + meta.exports_ref = Ref.None; + } + + // Internal "import()" of async ESM + if (record.kind == .dynamic and meta.is_wrapper_async) { + p.printSymbol(meta.wrapper_ref); + p.print("()"); + + if (!meta.exports_ref.isNull()) { + _ = p.printDotThenPrefix(); + p.printSymbol(meta.exports_ref); + p.printDotThenSuffix(); + } + return; + } + + // Internal "require()" or "import()" + if (record.kind == .dynamic) { + p.printSpaceBeforeIdentifier(); + p.print("Promise.resolve()"); + level = p.printDotThenPrefix(); + defer p.printDotThenSuffix(); + } + + // Make sure the comma operator is propertly wrapped + if (!meta.exports_ref.isNull() and level.gte(.comma)) { + p.print("("); + defer p.print(")"); + } + + // Wrap this with a call to "__toModule()" if this is a CommonJS file + if (record.wrap_with_to_module) { + p.printSymbol(p.options.to_module_ref); + p.print("("); + defer p.print(")"); + } + + // Call the wrapper + p.printSymbol(meta.wrapper_ref); + p.print("()"); + + // Return the namespace object if this is an ESM file + if (!meta.exports_ref.isNull()) { + p.print(","); + p.printSpace(); + p.printSymbol(meta.exports_ref); + } + } + + pub fn printQuotedUTF8(p: *Printer, str: string, allow_backtick: bool) void { + const quote = p.bestQuoteCharForString(str, allow_backtick); + p.print(quote); + // fast path: small strings get a stack allocation + if (str.len < 128) { + var buf = [128]u16{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + const bufEnd = strings.toUTF16Buf(str, &buf); + p.printQuotedUTF16(buf[0..bufEnd], quote); + } else { + // slow path: big strings get a heap allocation + p.printQuotedUTF16(strings.toUTF16Alloc(str, p.allocator) catch unreachable, quote); + } + p.print(quote); + } + + pub fn canPrintIdentifier(p: *Printer, name: string) bool { + if (ascii_only) { + return js_lexer.isIdentifier(name) and !strings.containsNonBmpCodePoint(name); + } else { + return js_lexer.isIdentifier(name); + } + } + + pub fn canPrintIdentifierUTF16(p: *Printer, name: JavascriptString) bool { + if (ascii_only) { + return js_lexer.isIdentifierUTF16(name) and !strings.containsNonBmpCodePointUTF16(name); + } else { + return js_lexer.isIdentifierUTF16(name); + } + } + + pub fn printExpr(p: *Printer, expr: Expr, level: Level, _flags: ExprFlag) void { p.addSourceMapping(expr.loc); + var flags = _flags; switch (expr.data) { - .e_missing => |e| { - notimpl(); - }, + .e_missing => |e| {}, .e_undefined => |e| { - notimpl(); + p.printSpaceBeforeIdentifier(); + + p.printUndefined(level); }, .e_super => |e| { - notimpl(); + p.printSpaceBeforeIdentifier(); + p.print("super"); }, .e_null => |e| { - notimpl(); + p.printSpaceBeforeIdentifier(); + p.print("null"); }, .e_this => |e| { - notimpl(); + p.printSpaceBeforeIdentifier(); + p.print("this"); }, .e_spread => |e| { - notimpl(); + p.print("..."); + p.printExpr(e.value, .comma, ExprFlag.None()); }, .e_new_target => |e| { - notimpl(); + p.printSpaceBeforeIdentifier(); + p.print("new.target"); }, .e_import_meta => |e| { - notimpl(); + p.printSpaceBeforeIdentifier(); + p.print("import.meta"); }, .e_new => |e| { - notimpl(); + const has_pure_comment = e.can_be_unwrapped_if_unused; + const wrap = level.gte(.call) or (has_pure_comment and level.gte(.postfix)); + + if (wrap) { + p.print("("); + } + + if (has_pure_comment) { + p.print("/* @__PURE__ */ "); + } + + p.printSpaceBeforeIdentifier(); + p.print("new"); + p.printSpace(); + p.printExpr(e.target, .new, ExprFlag.ForbidCall()); + + if (e.args.len > 0 or level.gte(.postfix)) { + p.print("("); + + if (e.args.len > 0) { + var i: usize = 0; + p.printExpr(e.args[i], .comma, ExprFlag.None()); + i = 1; + + while (i < e.args.len) { + p.print(","); + p.printSpace(); + p.printExpr(e.args[i], .comma, ExprFlag.None()); + i += 1; + } + } + + p.print(")"); + } + + if (wrap) { + p.print(")"); + } }, .e_call => |e| { - notimpl(); + var wrap = level.gte(.new) or flags.forbid_call; + var target_flags = ExprFlag.None(); + if (e.optional_chain == null) { + target_flags = ExprFlag.HasNonOptionalChainParent(); + } else if (flags.has_non_optional_chain_parent) { + wrap = true; + } + + const has_pure_comment = e.can_be_unwrapped_if_unused; + if (has_pure_comment and level.gte(.postfix)) { + wrap = true; + } + + if (wrap) { + p.print("("); + } + + if (has_pure_comment) { + const was_stmt_start = p.stmt_start == p.js.len(); + p.print("/* @__PURE__ */ "); + if (was_stmt_start) { + p.stmt_start = p.js.lenI(); + } + } + // We don't ever want to accidentally generate a direct eval expression here + p.call_target = e.target.data; + if (!e.is_direct_eval and p.isUnboundEvalIdentifier(e.target)) { + p.print("(0, "); + p.printExpr(e.target, .postfix, ExprFlag.None()); + p.print(")"); + } else { + p.printExpr(e.target, .postfix, target_flags); + } + + if (e.optional_chain != null and (e.optional_chain orelse unreachable) == .start) { + p.print("?."); + } + p.print("("); + + if (e.args.len > 0) { + p.printExpr(e.args[0], .comma, ExprFlag.None()); + var i: usize = 1; + while (i < e.args.len) { + p.print(","); + p.printSpace(); + p.printExpr(e.args[i], .comma, ExprFlag.None()); + i += 1; + } + } + + p.print(")"); + if (wrap) { + p.print(")"); + } }, .e_require => |e| { - notimpl(); + p.printRequireOrImportExpr(e.import_record_index, &([_]G.Comment{}), level, flags); }, .e_require_or_require_resolve => |e| { - notimpl(); + const wrap = level.gte(.new) or flags.forbid_call; + if (wrap) { + p.print("("); + } + + if (p.options.rewrite_require_resolve) { + // require.resolve("../src.js") => new URL("/src.js", location.origin).href + // require.resolve is not available to the browser + // if we return the relative filepath, that could be inaccessible if they're viewing the development server + // on a different origin than where it's compiling + // instead of doing that, we make the following assumption: the assets are same-origin + p.printSpaceBeforeIdentifier(); + p.print("new URL("); + p.printQuotedUTF8(p.import_records[e.import_record_index].path.text, true); + p.print(", location.origin).href"); + } else { + p.printSpaceBeforeIdentifier(); + p.printQuotedUTF8(p.import_records[e.import_record_index].path.text, true); + } + + if (wrap) { + p.print(")"); + } }, .e_import => |e| { - notimpl(); + // Handle non-string expressions + if (Ref.isSourceIndexNull(e.import_record_index)) { + const wrap = level.gte(.new) or flags.forbid_call; + if (wrap) { + p.print("("); + } + p.printSpaceBeforeIdentifier(); + p.print("import("); + if (e.leading_interior_comments.len > 0) { + p.printNewline(); + p.options.indent += 1; + for (e.leading_interior_comments) |comment| { + p.printIndentedComment(comment.text); + } + p.printIndent(); + } + p.printExpr(e.expr, .comma, ExprFlag.None()); + + if (e.leading_interior_comments.len > 0) { + p.printNewline(); + p.options.indent -= 1; + p.printIndent(); + } + p.print(")"); + if (wrap) { + p.print(")"); + } + } else { + p.printRequireOrImportExpr(e.import_record_index, e.leading_interior_comments, level, flags); + } }, .e_dot => |e| { - notimpl(); + var wrap = false; + if (e.optional_chain == null) { + flags.has_non_optional_chain_parent = false; + } else { + if (flags.has_non_optional_chain_parent) { + wrap = true; + p.print("("); + } + + flags.has_non_optional_chain_parent = true; + } + p.printExpr(e.target, .postfix, flags); + // Zig compiler bug: e.optional_chain == null or e.optional_chain == .start causes broken LLVM IR + // https://github.com/ziglang/zig/issues/6059 + const isOptionalChain = (e.optional_chain orelse js_ast.OptionalChain.ccontinue) == js_ast.OptionalChain.start; + + if (isOptionalChain) { + p.print("?"); + } + if (p.canPrintIdentifier(e.name)) { + if (isOptionalChain and p.prev_num_end == p.js.len()) { + // "1.toString" is a syntax error, so print "1 .toString" instead + p.print(" "); + } + p.print("."); + p.addSourceMapping(e.name_loc); + p.printIdentifier(e.name); + } else { + p.print("["); + p.addSourceMapping(e.name_loc); + p.printQuotedUTF8(e.name, true); + p.print("]"); + } + + if (wrap) { + p.print(")"); + } }, .e_index => |e| { - notimpl(); + var wrap = false; + if (e.optional_chain == null) { + flags.has_non_optional_chain_parent = false; + } else { + if (flags.has_non_optional_chain_parent) { + wrap = true; + p.print("("); + } + flags.has_non_optional_chain_parent = false; + } + + p.printExpr(e.target, .postfix, flags); + + // Zig compiler bug: e.optional_chain == null or e.optional_chain == .start causes broken LLVM IR + // https://github.com/ziglang/zig/issues/6059 + const is_optional_chain_start = (e.optional_chain orelse js_ast.OptionalChain.ccontinue) == js_ast.OptionalChain.start; + + if (is_optional_chain_start) { + p.print("?."); + } + + switch (e.index.data) { + .e_private_identifier => |priv| { + if (is_optional_chain_start) { + p.print("."); + } + + p.printSymbol(priv.ref); + }, + else => { + p.print("["); + p.printExpr(e.index, .lowest, ExprFlag.None()); + p.print("]"); + }, + } + + if (wrap) { + p.print(")"); + } }, .e_if => |e| { - notimpl(); + const wrap = level.gte(.conditional); + if (wrap) { + p.print("("); + flags.forbid_in = !flags.forbid_in; + } + flags.forbid_in = true; + p.printExpr(e.test_, .conditional, flags); + p.printSpace(); + p.print("?"); + p.printExpr(e.yes, .yield, ExprFlag.None()); + p.printSpace(); + p.print(":"); + flags.forbid_in = true; + p.printExpr(e.no, .yield, flags); + if (wrap) { + p.print(")"); + } }, .e_arrow => |e| { - notimpl(); + const wrap = level.gte(.assign); + + if (wrap) { + p.print("("); + } + + if (e.is_async) { + p.printSpaceBeforeIdentifier(); + p.print("async"); + p.printSpace(); + } + + var wasPrinted = false; + if (e.body.stmts.len == 1 and e.prefer_expr) { + switch (e.body.stmts[0].data) { + .s_return => |ret| { + if (ret.value) |val| { + p.arrow_expr_start = p.js.lenI(); + p.printExpr(val, .comma, ExprFlag.None()); + wasPrinted = true; + } + }, + else => {}, + } + } + + if (!wasPrinted) { + p.printBlock(e.body.loc, e.body.stmts); + } + + if (wrap) { + p.print(")"); + } }, .e_function => |e| { - notimpl(); + const n = p.js.lenI(); + var wrap = p.stmt_start == n or p.export_default_start == n; + if (wrap) { + p.print("("); + } + + p.printSpaceBeforeIdentifier(); + if (e.func.flags.is_async) { + p.print("async "); + } + p.print("function"); + if (e.func.flags.is_generator) { + p.print("*"); + p.printSpace(); + } + + if (e.func.name) |sym| { + p.printSymbol(sym.ref orelse std.debug.panic("internal error: expected E.Function's name symbol to have a ref\n{s}", .{e.func})); + } + p.printFunc(e.func); + if (wrap) { + p.print(")"); + } }, .e_class => |e| { - notimpl(); + const n = p.js.lenI(); + var wrap = p.stmt_start == n or p.export_default_start == n; + if (wrap) { + p.print("("); + } + + p.printSpaceBeforeIdentifier(); + p.print("class"); + if (e.class_name) |name| { + p.printSymbol(name.ref orelse std.debug.panic("internal error: expected E.Class's name symbol to have a ref\n{s}", .{e})); + } + p.printClass(e.*); + if (wrap) { + p.print(")"); + } }, .e_array => |e| { - notimpl(); + p.print("["); + if (e.items.len > 0) { + if (!e.is_single_line) { + p.options.indent += 1; + } + + var i: usize = 0; + while (i < e.items.len) : (i += 1) { + if (i != 0) { + p.print(","); + if (e.is_single_line) { + p.printSpace(); + } + } + if (!e.is_single_line) { + p.printNewline(); + p.printIndent(); + } + p.printExpr(e.items[i], .comma, ExprFlag.None()); + + if (i == e.items.len - 1) { + // Make sure there's a comma after trailing missing items + switch (e.items[i].data) { + .e_missing => { + p.print(","); + }, + else => {}, + } + } + } + + if (!e.is_single_line) { + p.options.indent -= 1; + p.printNewline(); + p.printIndent(); + } + } + + p.print("]"); }, .e_object => |e| { - notimpl(); + const n = p.js.lenI(); + const wrap = p.stmt_start == n or p.arrow_expr_start == n; + + if (wrap) { + p.print("("); + } + p.print("{"); + if (e.properties.len > 0) { + if (!e.is_single_line) { + p.options.indent += 1; + } + + var i: usize = 0; + while (i < e.properties.len) : (i += 1) { + if (i != 0) { + p.print(","); + if (e.is_single_line) { + p.printSpace(); + } + } + + if (!e.is_single_line) { + p.printNewline(); + p.printIndent(); + } + p.printProperty(e.properties[i]); + } + + if (!e.is_single_line) { + p.options.indent -= 1; + p.printNewline(); + p.printIndent(); + } + } + p.print("}"); + if (wrap) { + p.print(")"); + } }, .e_boolean => |e| { p.printSpaceBeforeIdentifier(); @@ -559,10 +1128,49 @@ pub fn NewPrinter(comptime ascii_only: bool) type { p.print(c); }, .e_template => |e| { - notimpl(); + if (e.tag) |tag| { + // Optional chains are forbidden in template tags + if (expr.isOptionalChain()) { + p.print("("); + p.printExpr(tag, .lowest, ExprFlag.None()); + p.print(")"); + } else { + p.printExpr(tag, .postfix, ExprFlag.None()); + } + } + + p.print("`"); + if (e.tag != null) { + p.print(e.head_raw); + } else { + p.printQuotedUTF16(e.head, '`'); + } + + for (e.parts) |part| { + p.print("${"); + p.printExpr(part.value, .lowest, ExprFlag.None()); + p.print("}"); + if (e.tag != null) { + p.print(part.tail_raw); + } else { + p.printQuotedUTF16(part.tail, '`'); + } + } + p.print("`"); }, .e_reg_exp => |e| { - notimpl(); + const n = p.js.len(); + const tail = p.js.list.items[n - 1]; + + // Avoid forming a single-line comment + if (n > 0 and tail == '/') { + p.print(" "); + } + + p.print(e.value); + + // Need a space before the next identifier to avoid it turning into flags + p.prev_reg_exp_end = p.js.lenI(); }, .e_big_int => |e| { p.printSpaceBeforeIdentifier(); @@ -610,22 +1218,229 @@ pub fn NewPrinter(comptime ascii_only: bool) type { } }, .e_identifier => |e| { - notimpl(); + const name = p.renamer.nameForSymbol(e.ref); + const wrap = p.js.lenI() == p.for_of_init_start and strings.eql(name, "let"); + + if (wrap) { + p.print("("); + } + + p.printSpaceBeforeIdentifier(); + p.printIdentifier(name); + + if (wrap) { + p.print(")"); + } }, .e_import_identifier => |e| { - notimpl(); + // Potentially use a property access instead of an identifier + const ref = p.symbols.follow(e.ref); + var didPrint = false; + if (p.symbols.get(ref)) |symbol| { + if (symbol.import_item_status == .missing) { + p.printUndefined(level); + didPrint = true; + } else if (symbol.namespace_alias) |namespace| { + // this feels crashy + var wrap = false; + + if (p.call_target) |target| { + wrap = e.was_originally_identifier and target.e_import_identifier == e; + } + + if (wrap) { + p.print("(0, "); + } + + p.printSymbol(namespace.namespace_ref); + const alias = namespace.alias; + if (p.canPrintIdentifier(alias)) { + p.print("."); + p.printIdentifier(alias); + } else { + p.print("["); + p.printQuotedUTF8(alias, true); + p.print("]"); + } + didPrint = true; + + if (wrap) { + p.print(")"); + } + } + } + + if (!didPrint) { + p.printSymbol(e.ref); + } }, .e_await => |e| { - notimpl(); + const wrap = level.gte(.prefix); + + if (wrap) { + p.print("("); + } + + p.printSpaceBeforeIdentifier(); + p.print("await"); + p.printSpace(); + p.printExpr(e.value, Level.sub(.prefix, 1), ExprFlag.None()); + + if (wrap) { + p.print(")"); + } }, .e_yield => |e| { - notimpl(); + const wrap = level.gte(.assign); + if (wrap) { + p.print("("); + } + + p.printSpaceBeforeIdentifier(); + p.print("yield"); + + if (e.value) |val| { + if (e.is_star) { + p.print("*"); + } + p.printSpace(); + p.printExpr(val, .yield, ExprFlag.None()); + } + + if (wrap) { + p.print(")"); + } }, .e_unary => |e| { - notimpl(); + const entry: Op = Op.Table.get(e.op); + const wrap = level.gte(entry.level); + + if (wrap) { + p.print("("); + } + + if (!e.op.isPrefix()) { + p.printExpr(e.value, Op.Level.sub(.postfix, 1), ExprFlag.None()); + } + + if (entry.is_keyword) { + p.printSpaceBeforeIdentifier(); + p.print(entry.text); + p.printSpace(); + } else { + p.printSpaceBeforeOperator(e.op); + p.print(entry.text); + p.prev_op = e.op; + p.prev_op_end = p.js.lenI(); + } + + if (e.op.isPrefix()) { + p.printExpr(e.value, Op.Level.sub(.prefix, 1), ExprFlag.None()); + } + + if (wrap) { + p.print(")"); + } }, .e_binary => |e| { - notimpl(); + const entry: Op = Op.Table.get(e.op); + var wrap = level.gte(entry.level) or (e.op == Op.Code.bin_in and flags.forbid_in); + + // Destructuring assignments must be parenthesized + const n = p.js.lenI(); + if (n == p.stmt_start or n == p.arrow_expr_start) { + switch (e.left.data) { + .e_object => { + wrap = true; + }, + else => {}, + } + } + + var left_level = entry.level.sub(1); + var right_level = left_level; + + if (e.op.isRightAssociative()) { + left_level = entry.level; + } + + if (e.op.isLeftAssociative()) { + right_level = entry.level; + } + + switch (e.op) { + // "??" can't directly contain "||" or "&&" without being wrapped in parentheses + .bin_nullish_coalescing => { + switch (e.left.data) { + .e_binary => |left| { + switch (left.op) { + .bin_logical_and, .bin_logical_or => { + left_level = .prefix; + }, + else => {}, + } + }, + else => {}, + } + + switch (e.right.data) { + .e_binary => |right| { + switch (right.op) { + .bin_logical_and, .bin_logical_or => { + right_level = .prefix; + }, + else => {}, + } + }, + else => {}, + } + }, + // "**" can't contain certain unary expressions + .bin_pow => { + switch (e.left.data) { + .e_unary => |left| { + if (left.op.unaryAssignTarget() == .none) { + left_level = .call; + } + }, + .e_await, .e_undefined, .e_number => { + left_level = .call; + }, + else => {}, + } + }, + else => {}, + } + + // Special-case "#foo in bar" + if (e.op == .bin_in and @as(Expr.Tag, e.left.data) == .e_private_identifier) { + p.printSymbol(e.left.data.e_private_identifier.ref); + } else { + flags.forbid_in = true; + p.printExpr(e.left, left_level, flags); + } + + if (e.op != .bin_comma) { + p.printSpace(); + } + + if (entry.is_keyword) { + p.printSpaceBeforeIdentifier(); + p.print(entry.text); + } else { + p.printSpaceBeforeIdentifier(); + p.print(entry.text); + p.prev_op = e.op; + p.prev_op_end = p.js.lenI(); + } + + p.printSpace(); + flags.forbid_in = true; + p.printExpr(e.right, right_level, flags); + + if (wrap) { + p.print(")"); + } }, else => { std.debug.panic("Unexpected expression of type {s}", .{expr.data}); @@ -653,6 +1468,12 @@ pub fn NewPrinter(comptime ascii_only: bool) type { } } + pub fn printDotThenSuffix( + p: *Printer, + ) callconv(.Inline) void { + p.print(")"); + } + pub fn printProperty(p: *Printer, prop: G.Property) void { notimpl(); } @@ -697,7 +1518,7 @@ pub fn NewPrinter(comptime ascii_only: bool) type { .s_expr => |s| { p.printIndent(); p.stmt_start = p.js.lenI(); - p.printExpr(s.value, .lowest, .expr_result_is_unused); + p.printExpr(s.value, .lowest, ExprFlag.ExprResultIsUnused()); p.printSemicolonAfterStatement(); }, else => { @@ -706,6 +1527,44 @@ pub fn NewPrinter(comptime ascii_only: bool) type { } } + pub fn printIdentifier(p: *Printer, identifier: string) void { + if (ascii_only) { + quoteIdentifier(&p.js, identifier) catch unreachable; + } else { + p.print(identifier); + } + } + + pub fn printIdentifierUTF16(p: *Printer, name: JavascriptString) !void { + var temp = [_]u8{ 0, 0, 0, 0, 0, 0 }; + const n = name.len; + var i: usize = 0; + while (i < n) : (i += 1) { + var c: u21 = name[i]; + + if (c >= first_high_surrogate and c <= last_high_surrogate and i + 1 < n) { + const c2: u21 = name[i + 1]; + if (c2 >= first_low_surrogate and c2 <= last_low_surrogate) { + c = (c << 10) + c2 + (0x10000 - (first_high_surrogate << 10) - first_low_surrogate); + i += 1; + } + } + + if (ascii_only and c > last_ascii) { + if (c > last_low_surrogate and c <= 0xFFFF) { + temp = [_]u8{ '\\', 'u', hex_chars[c >> 12], hex_chars[(c >> 8) & 15], hex_chars[(c >> 4) & 15], hex_chars[c & 15] }; + p.print(&temp); + } else { + std.debug.panic("Not implemented yet: unicode escapes in ascii only", .{}); + } + continue; + } + + const width = std.unicode.utf8Encode(c, temp); + p.print(temp[0..width]); + } + } + pub fn printIndentedComment(p: *Printer, _text: string) void { var text = _text; if (strings.startsWith(text, "/*")) { @@ -726,7 +1585,7 @@ pub fn NewPrinter(comptime ascii_only: bool) type { } } - pub fn init(allocator: *std.mem.Allocator, tree: Ast, symbols: Symbol.Map, opts: Options) !Printer { + pub fn init(allocator: *std.mem.Allocator, tree: Ast, symbols: Symbol.Map, opts: Options, linker: *Linker) !Printer { var js = try MutableString.init(allocator, 1024); return Printer{ .allocator = allocator, @@ -735,17 +1594,47 @@ pub fn NewPrinter(comptime ascii_only: bool) type { .symbols = symbols, .js = js, .writer = js.writer(), + .linker = linker, + .renamer = rename.Renamer{ + .symbols = symbols, + }, }; } }; } +// TODO: +pub fn quoteIdentifier(js: *MutableString, identifier: string) !void { + return try js.append(identifier); + // assert(identifier.len > 0); + // var utf8iter = std.unicode.Utf8Iterator{ .bytes = identifier, .i = 0 }; + // try js.growIfNeeded(identifier.len); + + // var init = utf8iter.nextCodepoint() orelse unreachable; + // var ascii_start: usize = if (init >= first_ascii and init <= last_ascii) 0 else std.math.maxInt(usize); + + // while (utf8iter.nextCodepoint()) |code_point| { + // switch (code_point) { + // first_ascii...last_ascii => {}, + // else => { + // ascii_start = utf8iter.i; + // }, + // } + // } +} + const UnicodePrinter = NewPrinter(false); const AsciiPrinter = NewPrinter(true); -pub fn printAst(allocator: *std.mem.Allocator, tree: Ast, symbols: js_ast.Symbol.Map, ascii_only: bool, opts: Options) !PrintResult { +pub fn printAst(allocator: *std.mem.Allocator, tree: Ast, symbols: js_ast.Symbol.Map, ascii_only: bool, opts: Options, linker: *Linker) !PrintResult { if (ascii_only) { - var printer = try AsciiPrinter.init(allocator, tree, symbols, opts); + var printer = try AsciiPrinter.init( + allocator, + tree, + symbols, + opts, + linker, + ); for (tree.parts) |part| { for (part.stmts) |stmt| { try printer.printStmt(stmt); @@ -756,7 +1645,13 @@ pub fn printAst(allocator: *std.mem.Allocator, tree: Ast, symbols: js_ast.Symbol .js = printer.js.toOwnedSlice(), }; } else { - var printer = try UnicodePrinter.init(allocator, tree, symbols, opts); + var printer = try UnicodePrinter.init( + allocator, + tree, + symbols, + opts, + linker, + ); for (tree.parts) |part| { for (part.stmts) |stmt| { try printer.printStmt(stmt); diff --git a/src/json_parser.zig b/src/json_parser.zig index f42fbc89f..a46375929 100644 --- a/src/json_parser.zig +++ b/src/json_parser.zig @@ -22,7 +22,6 @@ const StmtNodeList = js_ast.StmtNodeList; const BindingNodeList = js_ast.BindingNodeList; const assert = std.debug.assert; -const Ref = js_ast.Ref; const LocRef = js_ast.LocRef; const S = js_ast.S; const B = js_ast.B; @@ -239,8 +238,9 @@ fn expectPrintedJSON(_contents: string, expected: string) void { if (log.msgs.items.len > 0) { std.debug.panic("--FAIL--\nExpr {s}\nLog: {s}\n--FAIL--", .{ expr, log.msgs.items[0].data.text }); } + var linker = @import("linker.zig").Linker{}; - const result = js_printer.printAst(alloc.dynamic, tree, symbol_map, true, js_printer.Options{ .to_module_ref = Ref{ .inner_index = 0 } }) catch unreachable; + const result = js_printer.printAst(alloc.dynamic, tree, symbol_map, true, js_printer.Options{ .to_module_ref = Ref{ .inner_index = 0 } }, &linker) catch unreachable; var js = result.js; diff --git a/src/linker.zig b/src/linker.zig new file mode 100644 index 000000000..884dfe6ee --- /dev/null +++ b/src/linker.zig @@ -0,0 +1,11 @@ +const std = @import("std"); +const fs = @import("fs.zig"); +usingnamespace @import("ast/base.zig"); + +pub const Linker = struct { + // fs: fs.FileSystem, + // TODO: + pub fn requireOrImportMetaForSource(c: Linker, source_index: Ref.Int) RequireOrImportMeta { + return RequireOrImportMeta{}; + } +}; diff --git a/src/logger.zig b/src/logger.zig index 0aa37bdc8..a914f4741 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -273,14 +273,10 @@ pub const Source = struct { pub const ErrorPosition = struct { line_start: usize, line_end: usize, column_count: usize, line_count: usize }; pub fn initFile(file: fs.File, allocator: *std.mem.Allocator) Source { - std.debug.assert(file.contents != null); var name = file.path.name; var identifier_name = name.nonUniqueNameString(allocator) catch unreachable; - if (file.contents) |contents| { - return Source{ .path = file.path, .identifier_name = identifier_name, .contents = contents }; - } else { - std.debug.panic("Expected file.contents to not be null. {s}", .{file}); - } + + return Source{ .path = file.path, .identifier_name = identifier_name, .contents = file.contents }; } pub fn initPathString(pathString: string, contents: string) Source { diff --git a/src/main.zig b/src/main.zig index 5410b0e2a..6144fdf24 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,6 +6,7 @@ const options = @import("options.zig"); const js_parser = @import("js_parser.zig"); const js_printer = @import("js_printer.zig"); const js_ast = @import("js_ast.zig"); +const linker = @import("linker.zig"); pub fn main() anyerror!void { try alloc.setup(std.heap.page_allocator); @@ -28,8 +29,15 @@ pub fn main() anyerror!void { var source = logger.Source.initFile(opts.entry_point, alloc.dynamic); var parser = try js_parser.Parser.init(opts, &log, &source, alloc.dynamic); var res = try parser.parse(); - - const printed = try js_printer.printAst(alloc.dynamic, res.ast, js_ast.Symbol.Map{}, false, js_printer.Options{ .to_module_ref = js_ast.Ref{ .inner_index = 0 } }); + var _linker = linker.Linker{}; + const printed = try js_printer.printAst( + alloc.dynamic, + res.ast, + js_ast.Symbol.Map{}, + false, + js_printer.Options{ .to_module_ref = js_ast.Ref{ .inner_index = 0 } }, + &_linker, + ); std.debug.print("{s}\n{s}", .{ res, printed }); } diff --git a/src/options.zig b/src/options.zig index f58149efb..35ea37e66 100644 --- a/src/options.zig +++ b/src/options.zig @@ -56,7 +56,10 @@ pub const TransformOptions = struct { var filesystemCache = std.StringHashMap(fs.File).init(allocator); - var entryPoint = fs.File{ .path = fs.Path.init(entryPointName), .contents = code, .mtime = null }; + var entryPoint = fs.File{ + .path = fs.Path.init(entryPointName), + .contents = code, + }; var define = std.StringHashMap(string).init(allocator); try define.ensureCapacity(1); diff --git a/src/renamer.zig b/src/renamer.zig index ed811bd1c..c39a51847 100644 --- a/src/renamer.zig +++ b/src/renamer.zig @@ -1,4 +1,6 @@ const js_ast = @import("js_ast.zig"); +usingnamespace @import("strings.zig"); +const std = @import("std"); pub const Renamer = struct { symbols: js_ast.Symbol.Map, diff --git a/src/string_immutable.zig b/src/string_immutable.zig index abfeec072..c1197e6ef 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -52,9 +52,7 @@ pub fn endsWithAny(self: string, str: string) bool { return false; } -pub fn lastNonwhitespace(self: string, str: string) bool { - -} +pub fn lastNonwhitespace(self: string, str: string) bool {} pub fn endsWithAnyComptime(self: string, comptime str: string) bool { if (str.len < 10) { @@ -90,3 +88,82 @@ pub fn index(self: string, str: string) i32 { pub fn eqlUtf16(comptime self: string, other: JavascriptString) bool { return std.mem.eql(u16, std.unicode.utf8ToUtf16LeStringLiteral(self), other); } + +pub fn toUTF16Buf(in: string, out: []u16) usize { + var utf8Iterator = std.unicode.Utf8Iterator{ .bytes = in, .i = 0 }; + + var c: u21 = 0; + var i: usize = 0; + while (utf8Iterator.nextCodepoint()) |code_point| { + switch (code_point) { + 0...0xFFFF => { + out[i] = @intCast(u16, code_point); + i += 1; + }, + else => { + c = code_point - 0x10000; + out[i] = @intCast(u16, 0xD800 + ((c >> 10) & 0x3FF)); + i += 1; + out[i] = @intCast(u16, 0xDC00 + (c & 0x3FF)); + i += 1; + }, + } + } + + return utf8Iterator.i; +} + +pub fn toUTF16Alloc(in: string, allocator: *std.mem.Allocator) !JavascriptString { + var utf8Iterator = std.unicode.Utf8Iterator{ .bytes = in, .i = 0 }; + var out = try std.ArrayList(u16).initCapacity(allocator, in.len); + + var c: u21 = 0; + var i: usize = 0; + while (utf8Iterator.nextCodepoint()) |code_point| { + switch (code_point) { + 0...0xFFFF => { + try out.append(@intCast(u16, code_point)); + }, + else => { + c = code_point - 0x10000; + try out.append(@intCast(u16, 0xD800 + ((c >> 10) & 0x3FF))); + try out.append(@intCast(u16, 0xDC00 + (c & 0x3FF))); + }, + } + } + + return out.toOwnedSlice(); +} + +pub fn containsNonBmpCodePoint(text: string) bool { + var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 }; + + while (iter.nextCodepoint()) |codepoint| { + if (codepoint > 0xFFFF) { + return true; + } + } + + return false; +} + +pub fn containsNonBmpCodePointUTF16(_text: JavascriptString) bool { + const n = _text.len; + if (n > 0) { + var i: usize = 0; + var c: u16 = 0; + var c2: u16 = 0; + var text = _text[0 .. n - 1]; + while (i < n - 1) : (i += 1) { + c = text[i]; + if (c >= 0xD800 and c <= 0xDBFF) { + c2 = text[i + 1]; + if (c2 >= 0xDC00 and c2 <= 0xDFFF) { + return true; + } + } + } + } + + return false; +} |