diff options
Diffstat (limited to 'src/js_printer.zig')
-rw-r--r-- | src/js_printer.zig | 999 |
1 files changed, 947 insertions, 52 deletions
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); |