diff options
author | 2021-04-29 20:22:25 -0700 | |
---|---|---|
committer | 2021-04-29 20:22:25 -0700 | |
commit | 24d1479ea825cfc6c7ec8f74780bc72b7cd6bc8e (patch) | |
tree | 4606fc2c9bb798717c5e06b5ca825b5f54df9947 /src | |
parent | 3731376943862e17646b477bc98ce3871f064e99 (diff) | |
download | bun-24d1479ea825cfc6c7ec8f74780bc72b7cd6bc8e.tar.gz bun-24d1479ea825cfc6c7ec8f74780bc72b7cd6bc8e.tar.zst bun-24d1479ea825cfc6c7ec8f74780bc72b7cd6bc8e.zip |
hm
Former-commit-id: 2567243c8db7a60a5ba8ca7c662beca080cfa4f4
Diffstat (limited to 'src')
-rw-r--r-- | src/defines.zig | 11 | ||||
-rw-r--r-- | src/js_ast.zig | 70 | ||||
-rw-r--r-- | src/js_parser.zig | 1447 | ||||
-rw-r--r-- | src/main.zig | 12 | ||||
-rw-r--r-- | src/string_immutable.zig | 2 |
5 files changed, 1492 insertions, 50 deletions
diff --git a/src/defines.zig b/src/defines.zig index 173e5dd0f..740d0d80e 100644 --- a/src/defines.zig +++ b/src/defines.zig @@ -30,6 +30,7 @@ pub const UserDefines = std.StringHashMap(DefineData); pub const DefineData = struct { value: js_ast.Expr.Data, + valueless: bool = false, original_name: ?string = null, // True if accessing this value is known to not have any side effects. For @@ -46,6 +47,10 @@ pub const DefineData = struct { // So we can create just one struct for it. pub const GlobalDefineData = DefineData{}; + pub fn isUndefined(self: *const DefineData) bool { + return self.valueless; + } + pub fn merge(a: DefineData, b: DefineData) DefineData { return DefineData{ .value = b.value, @@ -142,7 +147,7 @@ pub const Define = struct { dots: std.StringHashMap([]DotDefine), allocator: *std.mem.Allocator, - pub fn init(allocator: *std.mem.Allocator, user_defines: UserDefines) !*@This() { + pub fn init(allocator: *std.mem.Allocator, _user_defines: ?UserDefines) !*@This() { var define = try allocator.create(Define); define.allocator = allocator; define.identifiers = std.StringHashMap(IdentifierDefine).init(allocator); @@ -155,7 +160,7 @@ pub const Define = struct { var ident_define = IdentifierDefine{ .value = val, }; - var value_define = DefineData{ .value = val }; + var value_define = DefineData{ .value = val, .valueless = true }; // Step 1. Load the globals into the hash tables for (GlobalDefinesKey) |global| { if (global.len == 1) { @@ -204,7 +209,7 @@ pub const Define = struct { // Step 3. Load user data into hash tables // At this stage, user data has already been validated. - if (user_defines.count() > 0) { + if (_user_defines) |user_defines| { var iter = user_defines.iterator(); while (iter.next()) |user_define| { // If it has a dot, then it's a DotDefine. diff --git a/src/js_ast.zig b/src/js_ast.zig index 4701020fd..fb7f26900 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -608,11 +608,11 @@ pub const Symbol = struct { }; pub fn isKindPrivate(kind: Symbol.Kind) bool { - return kind >= Symbol.Kind.private_field and kind <= Symbol.Kind.private_static_get_set_pair; + return @enumToInt(kind) >= @enumToInt(Symbol.Kind.private_field) and @enumToInt(kind) <= @enumToInt(Symbol.Kind.private_static_get_set_pair); } pub fn isKindHoisted(kind: Symbol.Kind) bool { - return kind == Symbol.Kind.hoisted or kind == Symbol.Kind.hoisted_function; + return @enumToInt(kind) == @enumToInt(Symbol.Kind.hoisted) or @enumToInt(kind) == @enumToInt(Symbol.Kind.hoisted_function); } pub fn isHoisted(self: *Symbol) bool { @@ -1222,6 +1222,14 @@ pub const Expr = struct { pub const EFlags = enum { none, ts_decorator }; + pub fn extractNumericValues(left: Expr.Data, right: Expr.Data) ?[2]f64 { + if (!(@as(Expr.Tag, left) == .e_number and @as(Expr.Tag, right) == .e_number)) { + return null; + } + + return [2]f64{ left.e_number.value, right.e_number.value }; + } + pub fn isAnonymousNamed(e: *Expr) bool { switch (e.data) { .e_arrow => { @@ -2045,6 +2053,38 @@ pub const Expr = struct { } }; + pub fn isBoolean(a: Expr) bool { + switch (a.data) { + .e_boolean => { + return true; + }, + + .e_if => |ex| { + return isBoolean(ex.yes) and isBoolean(ex.no); + }, + .e_unary => |ex| { + return ex.op == .un_not or ex.op == .un_delete; + }, + .e_binary => |ex| { + switch (ex.op) { + .bin_strict_eq, .bin_strict_ne, .bin_loose_eq, .bin_loose_ne, .bin_lt, .bin_gt, .bin_le, .bin_ge, .bin_instanceof, .bin_in => { + return true; + }, + .bin_logical_or => { + return isBoolean(ex.left) and isBoolean(ex.right); + }, + .bin_logical_and => { + return isBoolean(ex.left) and isBoolean(ex.right); + }, + else => {}, + } + }, + else => {}, + } + + return false; + } + pub fn assign(a: Expr, b: Expr, allocator: *std.mem.Allocator) Expr { return alloc(allocator, E.Binary{ .op = .bin_assign, @@ -2052,16 +2092,16 @@ pub const Expr = struct { .right = b, }, a.loc); } - pub fn at(expr: *Expr, t: anytype, allocator: *std.mem.allocator) callconv(.Inline) Expr { - return alloc(allocator, t, loc); + pub fn at(expr: *Expr, t: anytype, allocator: *std.mem.Allocator) callconv(.Inline) Expr { + return alloc(allocator, t, expr.loc); } // Wraps the provided expression in the "!" prefix operator. The expression // will potentially be simplified to avoid generating unnecessary extra "!" // operators. For example, calling this with "!!x" will return "!x" instead // of returning "!!!x". - pub fn not(expr: Expr, allocator: *std.mem.Allocator) Expr { - return maybeSimplifyNot(&expr, allocator) orelse expr; + pub fn not(expr: *Expr, allocator: *std.mem.Allocator) Expr { + return maybeSimplifyNot(expr, allocator) orelse expr.*; } // The given "expr" argument should be the operand of a "!" prefix operator @@ -2086,16 +2126,16 @@ pub const Expr = struct { .e_function, .e_arrow, .e_reg_exp, - => |b| { + => { return expr.at(E.Boolean{ .value = false }, allocator); }, // "!!!a" => "!a" .e_unary => |un| { - if (un.op == Op.Code.un_not and isBooleanValue(un.value)) { - return un.value.*; + if (un.op == Op.Code.un_not and isBoolean(un.value)) { + return un.value; } }, - .e_binary => |*ex| { + .e_binary => |ex| { // TODO: evaluate whether or not it is safe to do this mutation since it's modifying in-place. // Make sure that these transformations are all safe for special values. // For example, "!(a < b)" is not the same as "a >= b" if a and/or b are @@ -2105,20 +2145,20 @@ pub const Expr = struct { ex.op = .bin_loose_ne; return expr.*; }, - Op.Code.bin_op_loose_ne => { + Op.Code.bin_loose_ne => { ex.op = .bin_loose_eq; return expr.*; }, - Op.Code.bin_op_strict_eq => { + Op.Code.bin_strict_eq => { ex.op = .bin_strict_ne; return expr.*; }, - Op.Code.bin_op_strict_ne => { + Op.Code.bin_strict_ne => { ex.op = .bin_strict_eq; return expr.*; }, - Op.Code.bin_op_comma => { - ex.right = ex.right.not(); + Op.Code.bin_comma => { + ex.right = ex.right.not(allocator); return expr.*; }, else => {}, diff --git a/src/js_parser.zig b/src/js_parser.zig index 5d3653eb5..5b5ffe949 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -13,6 +13,7 @@ const fs = @import("fs.zig"); usingnamespace @import("strings.zig"); usingnamespace @import("ast/base.zig"); usingnamespace js_ast.G; +usingnamespace @import("defines.zig"); const ImportKind = importRecord.ImportKind; const BindingNodeIndex = js_ast.BindingNodeIndex; @@ -39,6 +40,387 @@ const Op = js_ast.Op; const Scope = js_ast.Scope; const locModuleScope = logger.Loc.Empty; +pub fn locAfterOp(e: E.Binary) logger.Loc { + if (e.left.loc.start < e.right.loc.start) { + return e.right.loc; + } else { + // handle the case when we have transposed the operands + return e.left.loc; + } +} + +pub const SideEffects = enum { + could_have_side_effects, + no_side_effects, + + pub const Result = struct { + side_effects: SideEffects, + ok: bool = false, + value: bool = false, + }; + + pub fn toNumber(data: Expr.Data) ?f64 { + switch (data) { + .e_null => |e| { + return 0; + }, + .e_undefined => |e| { + return std.math.nan_f64; + }, + .e_boolean => |e| { + return if (e.value) 1.0 else 0.0; + }, + .e_number => |e| { + return e.value; + }, + else => {}, + } + + return null; + } + + pub fn isPrimitiveToReorder(data: Expr.Data) bool { + switch (data) { + .e_null, .e_undefined, .e_string, .e_boolean, .e_number, .e_big_int => { + return true; + }, + else => { + return false; + }, + } + } + + pub const Equality = struct { equal: bool = false, ok: bool = false }; + + // Returns "equal, ok". If "ok" is false, then nothing is known about the two + // values. If "ok" is true, the equality or inequality of the two values is + // stored in "equal". + pub fn eql(left: Expr.Data, right: Expr.Data) Equality { + var equality = Equality{}; + switch (left) { + .e_null => |l| { + equality.equal = @as(Expr.Tag, right) == Expr.Tag.e_null; + equality.ok = equality.equal; + }, + .e_undefined => |l| { + equality.equal = @as(Expr.Tag, right) == Expr.Tag.e_undefined; + equality.ok = equality.equal; + }, + .e_boolean => |l| { + equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_boolean; + equality.equal = l.value == right.e_boolean.value; + }, + .e_number => |l| { + equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_number; + equality.equal = l.value == right.e_number.value; + }, + .e_big_int => |l| { + equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_big_int; + equality.equal = strings.eql(l.value, right.e_big_int.value); + }, + .e_string => |l| { + equality.ok = @as(Expr.Tag, right) == Expr.Tag.e_string; + equality.equal = std.mem.eql(u16, l.value, right.e_string.value); + }, + else => {}, + } + + return equality; + } + + // Returns true if this expression is known to result in a primitive value (i.e. + // null, undefined, boolean, number, bigint, or string), even if the expression + // cannot be removed due to side effects. + pub fn isPrimitiveWithSideEffects(data: Expr.Data) bool { + switch (data) { + .e_null, .e_undefined, .e_boolean, .e_number, .e_big_int, .e_string => { + return true; + }, + .e_unary => |e| { + switch (e.op) { + // number or bigint + .un_pos, + .un_neg, + .un_cpl, + .un_pre_dec, + .un_pre_inc, + .un_post_dec, + .un_post_inc, + // boolean + .un_not, + .un_delete, + // undefined + .un_void, + // string + .un_typeof, + => { + return true; + }, + else => {}, + } + }, + .e_binary => |e| { + switch (e.op) { + // boolean + .bin_lt, + .bin_le, + .bin_gt, + .bin_ge, + .bin_in, + .bin_instanceof, + .bin_loose_eq, + .bin_loose_ne, + .bin_strict_eq, + .bin_strict_ne, + // string, number, or bigint + .bin_add, + .bin_add_assign, + // number or bigint + .bin_sub, + .bin_mul, + .bin_div, + .bin_rem, + .bin_pow, + .bin_sub_assign, + .bin_mul_assign, + .bin_div_assign, + .bin_rem_assign, + .bin_pow_assign, + .bin_shl, + .bin_shr, + .bin_u_shr, + .bin_shl_assign, + .bin_shr_assign, + .bin_u_shr_assign, + .bin_bitwise_or, + .bin_bitwise_and, + .bin_bitwise_xor, + .bin_bitwise_or_assign, + .bin_bitwise_and_assign, + .bin_bitwise_xor_assign, + => { + return true; + }, + + // These always return one of the arguments unmodified + .bin_logical_and, .bin_logical_or, .bin_nullish_coalescing, .bin_logical_and_assign, .bin_logical_or_assign, .bin_nullish_coalescing_assign => { + return isPrimitiveWithSideEffects(e.left.data) and isPrimitiveWithSideEffects(e.right.data); + }, + .bin_comma => { + return isPrimitiveWithSideEffects(e.right.data); + }, + } + }, + .e_if => { + return isPrimitiveWithSideEffects(e.yes.data) and isPrimitiveWithSideEffects(e.no.data); + }, + else => {}, + } + return false; + } + + // Returns true if the result of the "typeof" operator on this expression is + // statically determined and this expression has no side effects (i.e. can be + // removed without consequence). + pub fn toTypeof(data: Expr.Data) ?string { + switch (data) { + .e_null => { + return "object"; + }, + .e_undefined => { + return "undefined"; + }, + .e_boolean => { + return "boolean"; + }, + .e_number => { + return "number"; + }, + .e_big_int => { + return "bigint"; + }, + .e_string => { + return "string"; + }, + .e_function, .e_arrow => { + return "function"; + }, + else => {}, + } + + return null; + } + + pub fn toNullOrUndefined(exp: Expr.Data) Result { + switch (exp) { + // Never null or undefined + .e_boolean, .e_number, .e_string, .e_reg_exp, .e_function, .e_arrow, .e_big_int => { + return Result{ .value = false, .side_effects = SideEffects.no_side_effects, .ok = true }; + }, + + .e_object, .e_array, .e_class => { + return Result{ .value = false, .side_effects = .could_have_side_effects, .ok = true }; + }, + + // always anull or undefined + .e_null, .e_undefined => { + return Result{ .value = true, .side_effects = .could_have_side_effects, .ok = true }; + }, + + .e_unary => |e| { + switch (e.op) { + // Always number or bigint + .un_pos, .un_neg, .un_cpl, .un_pre_dec, .un_pre_inc, .un_post_dec, .un_post_inc => { + return Result{ .ok = true, .value = false, .side_effects = SideEffects.could_have_side_effects }; + }, + // Always undefined + .un_not, .un_typeof, .un_delete => { + return Result{ .value = true, .side_effects = .could_have_side_effects, .ok = true }; + }, + + .un_void => { + return Result{ .value = true, .side_effects = .could_have_side_effects, .ok = true }; + }, + + else => {}, + } + }, + + .e_binary => |e| { + switch (e.op) { + // always string or number or bigint + .bin_add, + .bin_add_assign, + // always number or bigint + .bin_sub, + .bin_mul, + .bin_div, + .bin_rem, + .bin_pow, + .bin_sub_assign, + .bin_mul_assign, + .bin_div_assign, + .bin_rem_assign, + .bin_pow_assign, + .bin_shl, + .bin_shr, + .bin_u_shr, + .bin_shl_assign, + .bin_shr_assign, + .bin_u_shr_assign, + .bin_bitwise_or, + .bin_bitwise_and, + .bin_bitwise_xor, + .bin_bitwise_or_assign, + .bin_bitwise_and_assign, + .bin_bitwise_xor_assign, + // always boolean + .bin_lt, + .bin_le, + .bin_gt, + .bin_ge, + .bin_in, + .bin_instanceof, + .bin_loose_eq, + .bin_loose_ne, + .bin_strict_eq, + .bin_strict_ne, + => { + return Result{ .ok = true, .value = false, .side_effects = SideEffects.could_have_side_effects }; + }, + + .bin_comma => { + const res = toNullOrUndefined(e.right.data); + if (res.ok) { + return Result{ .ok = true, .value = res.value, .side_effects = SideEffects.could_have_side_effects }; + } + }, + else => {}, + } + }, + else => {}, + } + + return Result{ .ok = false, .value = false, .side_effects = SideEffects.could_have_side_effects }; + } + + pub fn toBoolean(exp: Expr.Data) Result { + switch (exp) { + .e_null, .e_undefined => { + return Result{ .ok = true, .value = false, .side_effects = .no_side_effects }; + }, + .e_boolean => |e| { + return Result{ .ok = true, .value = e.value, .side_effects = .no_side_effects }; + }, + .e_number => |e| { + return Result{ .ok = true, .value = e.value != 0.0 and !std.math.isNan(e.value), .side_effects = .no_side_effects }; + }, + .e_big_int => |e| { + return Result{ .ok = true, .value = !strings.eql(e.value, "0"), .side_effects = .no_side_effects }; + }, + .e_string => |e| { + return Result{ .ok = true, .value = e.value.len > 0, .side_effects = .no_side_effects }; + }, + .e_function, .e_arrow, .e_reg_exp => { + return Result{ .ok = true, .value = true, .side_effects = .no_side_effects }; + }, + .e_object, .e_array, .e_class => { + return Result{ .ok = true, .value = true, .side_effects = .could_have_side_effects }; + }, + .e_unary => |e_| { + switch (e_.op) { + .un_void => { + return Result{ .ok = true, .value = false, .side_effects = .could_have_side_effects }; + }, + .un_typeof => { + // Never an empty string + + return Result{ .ok = true, .value = true, .side_effects = .could_have_side_effects }; + }, + .un_not => { + var result = toBoolean(e_.value.data); + if (result.ok) { + result.value = !result.value; + return result; + } + }, + else => {}, + } + }, + .e_binary => |e_| { + switch (e_.op) { + .bin_logical_or => { + // "anything || truthy" is truthy + const result = toBoolean(e_.right.data); + if (result.value and result.ok) { + return Result{ .ok = true, .value = true, .side_effects = .could_have_side_effects }; + } + }, + .bin_logical_and => { + // "anything && falsy" is falsy + const result = toBoolean(e_.right.data); + if (!result.value and result.ok) { + return Result{ .ok = true, .value = false, .side_effects = .could_have_side_effects }; + } + }, + .bin_comma => { + // "anything, truthy/falsy" is truthy/falsy + var result = toBoolean(e_.right.data); + if (result.ok) { + result.side_effects = .could_have_side_effects; + return result; + } + }, + else => {}, + } + }, + else => {}, + } + + return Result{ .ok = false, .value = false, .side_effects = SideEffects.could_have_side_effects }; + } +}; + const ExprOrLetStmt = struct { stmt_or_expr: js_ast.StmtOrExpr, decls: []G.Decl = &([_]G.Decl{}), @@ -77,6 +459,12 @@ const AsyncPrefixExpression = enum { } }; +const IdentifierOpts = struct { + assign_target: js_ast.AssignTarget = js_ast.AssignTarget.none, + is_delete_target: bool = false, + was_originally_identifier: bool = false, +}; + fn statementCaresAboutScope(stmt: Stmt) bool { switch (stmt.data) { .s_block, @@ -269,7 +657,6 @@ const EnumValueType = enum { string, numeric, }; -pub const Result = js_ast.Result; const ParenExprOpts = struct { async_range: logger.Range = logger.Range.None, @@ -425,6 +812,7 @@ pub const Parser = struct { lexer: js_lexer.Lexer, log: *logger.Log, source: logger.Source, + define: *Define, allocator: *std.mem.Allocator, p: ?*P, @@ -443,12 +831,12 @@ pub const Parser = struct { moduleType: ModuleType = ModuleType.esm, }; - pub fn parse(self: *Parser) !Result { + pub fn parse(self: *Parser) !js_ast.Result { if (self.p == null) { - self.p = try P.init(self.allocator, self.log, self.source, self.lexer, self.options); + self.p = try P.init(self.allocator, self.log, self.source, self.define, self.lexer, self.options); } - var result: Result = undefined; + var result: js_ast.Result = undefined; if (self.p) |p| { // Parse the file in the first pass, but do not bind symbols @@ -501,7 +889,7 @@ pub const Parser = struct { return result; } - pub fn init(transform: options.TransformOptions, log: *logger.Log, source: *logger.Source, allocator: *std.mem.Allocator) !Parser { + pub fn init(transform: options.TransformOptions, log: *logger.Log, source: *logger.Source, define: *Define, allocator: *std.mem.Allocator) !Parser { const lexer = try js_lexer.Lexer.init(log, source, allocator); return Parser{ .options = Options{ @@ -514,6 +902,7 @@ pub const Parser = struct { }, .allocator = allocator, .lexer = lexer, + .define = define, .source = source.*, .log = log, .p = null, @@ -562,6 +951,7 @@ pub const P = struct { allocator: *std.mem.Allocator, options: Parser.Options, log: *logger.Log, + define: *Define, source: logger.Source, lexer: js_lexer.Lexer, allow_in: bool = false, @@ -873,7 +1263,7 @@ pub const P = struct { }; } - pub fn recordUsage(p: *P, ref: *js_ast.Ref) void { + pub fn recordUsage(p: *P, ref: *const js_ast.Ref) void { // The use count stored in the symbol is used for generating symbol names // during minification. These counts shouldn't include references inside dead // code regions since those will be culled. @@ -1009,6 +1399,52 @@ pub const P = struct { return .forbidden; } + pub fn handleIdentifier(p: *P, loc: logger.Loc, ident: *E.Identifier, _original_name: ?string, opts: IdentifierOpts) Expr { + const ref = ident.ref; + + if ((opts.assign_target != .none or opts.is_delete_target) and p.symbols.items[ref.inner_index].kind == .import) { + // Create an error for assigning to an import namespace + const r = js_lexer.rangeOfIdentifier(&p.source, loc); + p.log.addRangeErrorFmt(p.source, r, p.allocator, "Cannot assign to import \"{s}\"", .{ + p.symbols.items[ref.inner_index].original_name, + }) catch unreachable; + } + + // Substitute an EImportIdentifier now if this is an import item + if (p.is_import_item.contains(ref)) { + return p.e( + E.ImportIdentifier{ .ref = ref, .was_originally_identifier = opts.was_originally_identifier }, + loc, + ); + } + + // Substitute a namespace export reference now if appropriate + if (p.options.ts) { + if (p.is_exported_inside_namespace.get(ref)) |ns_ref| { + const name = p.symbols.items[ref.inner_index].original_name; + + // If this is a known enum value, inline the value of the enum + if (p.known_enum_values.get(ns_ref)) |enum_values| { + if (enum_values.get(name)) |number| { + return p.e(E.Number{ .value = number }, loc); + } + } + + // Otherwise, create a property access on the namespace + p.recordUsage(&ns_ref); + + return p.e(E.Dot{ .target = p.e(E.Identifier{ .ref = ns_ref }, loc), .name = name, .name_loc = loc }, loc); + } + } + + if (_original_name) |original_name| { + const result = p.findSymbol(loc, original_name) catch unreachable; + ident.ref = result.ref; + } + + return p.e(ident, loc); + } + pub fn prepareForVisitPass(p: *P) !void { try p.pushScopeForVisitPass(js_ast.Scope.Kind.entry, locModuleScope); p.fn_or_arrow_data_visit.is_outside_fn_or_arrow = true; @@ -5964,6 +6400,10 @@ pub const P = struct { return p.e(E.Missing{}, logger.Loc.Empty); } + pub fn jsxStringsToMemberExpression(p: *P, loc: logger.Loc, fragment: string) Expr { + notimpl(); + } + // Note: The caller has already parsed the "import" keyword pub fn parseImportExpr(p: *P, loc: logger.Loc, level: Level) Expr { // Parse an "import.meta" expression @@ -6148,36 +6588,909 @@ pub const P = struct { // } }, - .e_import_meta => |exp| {}, + .e_import_meta => |exp| { + const is_delete_target = exp == p.delete_target.e_import_meta; + + if (p.define.dots.get("meta")) |meta| { + for (meta) |define| { + if (p.isDotDefineMatch(expr, define.parts)) { + // Substitute user-specified defines + return p.valueForDefine(expr.loc, in.assign_target, is_delete_target, &define.data); + } + } + } + + if (!p.import_meta_ref.isNull()) { + p.recordUsage(&p.import_meta_ref); + return p.e(E.Identifier{ .ref = p.import_meta_ref }, expr.loc); + } + }, .e_spread => |exp| { - return p.visitExpr(exp.value); + exp.value = p.visitExpr(exp.value); + }, + .e_identifier => |e_| { + const is_delete_target = e_ == p.delete_target.e_identifier; + + const name = p.loadNameFromRef(e_.ref); + if (p.isStrictMode() and js_lexer.StrictModeReservedWords.has(name)) { + p.markStrictModeFeature(.reserved_word, js_lexer.rangeOfIdentifier(&p.source, expr.loc), name) catch unreachable; + } + + const result = p.findSymbol(expr.loc, name) catch unreachable; + + e_.must_keep_due_to_with_stmt = result.is_inside_with_scope; + e_.ref = result.ref; + + // Handle assigning to a constant + if (in.assign_target != .none and p.symbols.items[result.ref.inner_index].kind == .cconst) { + const r = js_lexer.rangeOfIdentifier(&p.source, expr.loc); + p.log.addRangeErrorFmt(p.source, r, p.allocator, "Cannot assign to {s} because it is a constant", .{name}) catch unreachable; + } + + var original_name: ?string = null; + + // Substitute user-specified defines for unbound symbols + if (p.symbols.items[e_.ref.inner_index].kind == .unbound and !result.is_inside_with_scope and e_ != p.delete_target.e_identifier) { + if (p.define.identifiers.get(name)) |def| { + if (!def.isUndefined()) { + const newvalue = p.valueForDefine(expr.loc, in.assign_target, is_delete_target, &def); + + // Don't substitute an identifier for a non-identifier if this is an + // assignment target, since it'll cause a syntax error + if (@as(Expr.Tag, newvalue.data) == .e_identifier or in.assign_target == .none) { + return newvalue; + } + + original_name = def.original_name; + } + + // Copy the side effect flags over in case this expression is unused + if (def.can_be_removed_if_unused) { + e_.can_be_removed_if_unused = true; + } + if (def.call_can_be_unwrapped_if_unused and !p.options.ignore_dce_annotations) { + e_.call_can_be_unwrapped_if_unused = true; + } + } + } + + return p.handleIdentifier(expr.loc, e_, original_name, IdentifierOpts{ + .assign_target = in.assign_target, + .is_delete_target = is_delete_target, + .was_originally_identifier = true, + }); + }, + .e_private_identifier => |e_| { + p.panic("Unexpected private identifier. This is an internal error - not your fault.", .{}); + }, + .e_jsx_element => |e_| { + const tag = tagger: { + if (e_.tag) |_tag| { + break :tagger p.visitExpr(_tag); + } else { + break :tagger p.jsxStringsToMemberExpression(expr.loc, p.options.jsx.fragment); + } + }; + + // Visit properties + var i: usize = 0; + while (i < e_.properties.len) : (i += 1) { + const property = e_.properties[i]; + if (property.kind != .spread) { + e_.properties[i].key = p.visitExpr(e_.properties[i].key.?); + } + + if (property.value != null) { + e_.properties[i].value = p.visitExpr(e_.properties[i].value.?); + } + + if (property.initializer != null) { + e_.properties[i].initializer = p.visitExpr(e_.properties[i].initializer.?); + } + } + + // Arguments to createElement() + const args = p.allocator.alloc(Expr, 1 + e_.children.len) catch unreachable; + i = 1; + if (e_.properties.len > 0) { + args[0] = p.e(E.Object{ .properties = e_.properties }, expr.loc); + } else { + args[0] = p.e(E.Null{}, expr.loc); + } + + for (e_.children) |child| { + args[i] = p.visitExpr(child); + i += 1; + } + + // Call createElement() + return p.e(E.Call{ + .target = p.jsxStringsToMemberExpression(expr.loc, p.options.jsx.factory), + .args = args, + // Enable tree shaking + .can_be_unwrapped_if_unused = !p.options.ignore_dce_annotations, + }, expr.loc); + }, + + .e_template => |e_| { + if (e_.tag) |tag| { + e_.tag = p.visitExpr(tag); + } + + var i: usize = 0; + while (i < e_.parts.len) : (i += 1) { + e_.parts[i].value = p.visitExpr(e_.parts[i].value); + } + }, + + .e_binary => |e_| { + switch (e_.left.data) { + // Special-case private identifiers + .e_private_identifier => |private| { + if (e_.op == .bin_in) { + const name = p.loadNameFromRef(private.ref); + const result = p.findSymbol(e_.left.loc, name) catch unreachable; + private.ref = result.ref; + + // Unlike regular identifiers, there are no unbound private identifiers + const symbol: Symbol = p.symbols.items[result.ref.inner_index]; + if (!Symbol.isKindPrivate(symbol.kind)) { + const r = logger.Range{ .loc = e_.left.loc, .len = @intCast(i32, name.len) }; + p.log.addRangeErrorFmt(p.source, r, p.allocator, "Private name \"{s}\" must be declared in an enclosing class", .{name}) catch unreachable; + } + + e_.right = p.visitExpr(e_.right); + // privateSymbolNeedsToBeLowered + return expr; + } + }, + else => {}, + } + + const is_call_target = e_ == p.call_target.e_binary; + const is_stmt_expr = e_ == p.stmt_expr_value.e_binary; + const was_anonymous_named_expr = p.isAnonymousNamedExpr(e_.right); + + e_.left = p.visitExprInOut(e_.left, ExprIn{ + .assign_target = e_.op.binaryAssignTarget(), + }); + + // Mark the control flow as dead if the branch is never taken + switch (e_.op) { + .bin_logical_or => { + const side_effects = SideEffects.toBoolean(e_.left.data); + if (side_effects.ok and side_effects.value) { + // "true || dead" + const old = p.is_control_flow_dead; + p.is_control_flow_dead = true; + e_.right = p.visitExpr(e_.right); + p.is_control_flow_dead = old; + } else { + e_.right = p.visitExpr(e_.right); + } + }, + .bin_logical_and => { + const side_effects = SideEffects.toBoolean(e_.left.data); + if (side_effects.ok and side_effects.value) { + // "false && dead" + const old = p.is_control_flow_dead; + p.is_control_flow_dead = true; + e_.right = p.visitExpr(e_.right); + p.is_control_flow_dead = old; + } else { + e_.right = p.visitExpr(e_.right); + } + }, + .bin_nullish_coalescing => { + const side_effects = SideEffects.toNullOrUndefined(e_.left.data); + if (side_effects.ok and side_effects.value) { + // "false && dead" + const old = p.is_control_flow_dead; + p.is_control_flow_dead = true; + e_.right = p.visitExpr(e_.right); + p.is_control_flow_dead = old; + } else { + e_.right = p.visitExpr(e_.right); + } + }, + else => { + e_.right = p.visitExpr(e_.right); + }, + } + + // Always put constants on the right for equality comparisons to help + // reduce the number of cases we have to check during pattern matching. We + // can only reorder expressions that do not have any side effects. + switch (e_.op) { + .bin_loose_eq, .bin_loose_ne, .bin_strict_eq, .bin_strict_ne => { + if (SideEffects.isPrimitiveToReorder(e_.left.data) and !SideEffects.isPrimitiveToReorder(e_.right.data)) { + const _left = e_.left; + const _right = e_.right; + e_.left = _right; + e_.right = _left; + } + }, + else => {}, + } + + switch (e_.op) { + .bin_comma => { + // notimpl(); + }, + .bin_loose_eq => { + const equality = SideEffects.eql(e_.left.data, e_.right.data); + if (equality.ok) { + return p.e( + E.Boolean{ .value = equality.equal }, + expr.loc, + ); + } + + // const after_op_loc = locAfterOp(e_.); + // TODO: warn about equality check + // TODO: warn about typeof string + + }, + .bin_strict_eq => { + const equality = SideEffects.eql(e_.left.data, e_.right.data); + if (equality.ok) { + return p.e(E.Boolean{ .value = equality.ok }, expr.loc); + } + + // const after_op_loc = locAfterOp(e_.); + // TODO: warn about equality check + // TODO: warn about typeof string + }, + .bin_loose_ne => { + const equality = SideEffects.eql(e_.left.data, e_.right.data); + if (equality.ok) { + return p.e(E.Boolean{ .value = !equality.ok }, expr.loc); + } + // const after_op_loc = locAfterOp(e_.); + // TODO: warn about equality check + // TODO: warn about typeof string + + // "x != void 0" => "x != null" + if (@as(Expr.Tag, e_.right.data) == .e_undefined) { + e_.right = p.e(E.Null{}, e_.right.loc); + } + }, + .bin_strict_ne => { + const equality = SideEffects.eql(e_.left.data, e_.right.data); + if (equality.ok) { + return p.e(E.Boolean{ .value = !equality.ok }, expr.loc); + } + }, + .bin_nullish_coalescing => { + const nullorUndefined = SideEffects.toNullOrUndefined(e_.left.data); + if (!nullorUndefined.value) { + return e_.left; + } else if (nullorUndefined.side_effects == .no_side_effects) { + // TODO: + // "(null ?? fn)()" => "fn()" + // "(null ?? this.fn)" => "this.fn" + // "(null ?? this.fn)()" => "(0, this.fn)()" + + } + }, + .bin_logical_or => { + const side_effects = SideEffects.toBoolean(e_.left.data); + if (side_effects.ok and side_effects.value) { + return e_.left; + } else if (side_effects.ok) { + // TODO: + // "(0 || fn)()" => "fn()" + // "(0 || this.fn)" => "this.fn" + // "(0 || this.fn)()" => "(0, this.fn)()" + } + }, + .bin_logical_and => { + const side_effects = SideEffects.toBoolean(e_.left.data); + if (side_effects.ok) { + return e_.left; + } + + // TODO: + // "(1 && fn)()" => "fn()" + // "(1 && this.fn)" => "this.fn" + // "(1 && this.fn)()" => "(0, this.fn)()" + }, + .bin_add => { + if (p.should_fold_numeric_constants) { + if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + return p.e(E.Number{ .value = vals[0] + vals[1] }, expr.loc); + } + } + + // TODO: fold string addition + }, + .bin_sub => { + if (p.should_fold_numeric_constants) { + if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + return p.e(E.Number{ .value = vals[0] - vals[1] }, expr.loc); + } + } + }, + .bin_mul => { + if (p.should_fold_numeric_constants) { + if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + return p.e(E.Number{ .value = vals[0] * vals[1] }, expr.loc); + } + } + }, + .bin_div => { + if (p.should_fold_numeric_constants) { + if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + return p.e(E.Number{ .value = vals[0] / vals[1] }, expr.loc); + } + } + }, + .bin_rem => { + if (p.should_fold_numeric_constants) { + if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + // is this correct? + return p.e(E.Number{ .value = std.math.mod(f64, vals[0], vals[1]) catch 0.0 }, expr.loc); + } + } + }, + .bin_pow => { + if (p.should_fold_numeric_constants) { + if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + return p.e(E.Number{ .value = std.math.pow(f64, vals[0], vals[1]) }, expr.loc); + } + } + }, + .bin_shl => { + // TODO: + // if (p.should_fold_numeric_constants) { + // if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + // return p.e(E.Number{ .value = ((@floatToInt(i32, vals[0]) << @floatToInt(u32, vals[1])) & 31) }, expr.loc); + // } + // } + }, + .bin_shr => { + // TODO: + // if (p.should_fold_numeric_constants) { + // if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + // return p.e(E.Number{ .value = ((@floatToInt(i32, vals[0]) >> @floatToInt(u32, vals[1])) & 31) }, expr.loc); + // } + // } + }, + .bin_u_shr => { + // TODO: + // if (p.should_fold_numeric_constants) { + // if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + // return p.e(E.Number{ .value = ((@floatToInt(i32, vals[0]) >> @floatToInt(u32, vals[1])) & 31) }, expr.loc); + // } + // } + }, + .bin_bitwise_and => { + // TODO: + // if (p.should_fold_numeric_constants) { + // if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + // return p.e(E.Number{ .value = ((@floatToInt(i32, vals[0]) >> @floatToInt(u32, vals[1])) & 31) }, expr.loc); + // } + // } + }, + .bin_bitwise_or => { + // TODO: + // if (p.should_fold_numeric_constants) { + // if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + // return p.e(E.Number{ .value = ((@floatToInt(i32, vals[0]) >> @floatToInt(u32, vals[1])) & 31) }, expr.loc); + // } + // } + }, + .bin_bitwise_xor => { + // TODO: + // if (p.should_fold_numeric_constants) { + // if (Expr.extractNumericValues(e_.left.data, e_.right.data)) |vals| { + // return p.e(E.Number{ .value = ((@floatToInt(i32, vals[0]) >> @floatToInt(u32, vals[1])) & 31) }, expr.loc); + // } + // } + }, + // --------------------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------------- + .bin_assign => { + + // Optionally preserve the name + if (@as(Expr.Tag, e_.left.data) == .e_identifier) { + e_.right = p.maybeKeepExprSymbolName(e_.right, p.symbols.items[e_.left.data.e_identifier.ref.inner_index].original_name, was_anonymous_named_expr); + } + }, + .bin_add_assign => { + // notimpl(); + }, + .bin_sub_assign => { + // notimpl(); + }, + .bin_mul_assign => { + // notimpl(); + }, + .bin_div_assign => { + // notimpl(); + }, + .bin_rem_assign => { + // notimpl(); + }, + .bin_pow_assign => { + // notimpl(); + }, + .bin_shl_assign => { + // notimpl(); + }, + .bin_shr_assign => { + // notimpl(); + }, + .bin_u_shr_assign => { + // notimpl(); + }, + .bin_bitwise_or_assign => { + // notimpl(); + }, + .bin_bitwise_and_assign => { + // notimpl(); + }, + .bin_bitwise_xor_assign => { + // notimpl(); + }, + .bin_nullish_coalescing_assign => { + // notimpl(); + }, + .bin_logical_and_assign => { + // notimpl(); + }, + .bin_logical_or_assign => { + // notimpl(); + }, + else => {}, + } + }, + .e_index => |e_| { + const is_call_target = e_ == p.call_target.e_index; + const is_delete_target = e_ == p.delete_target.e_index; + + const target = p.visitExprInOut(e_.target, ExprIn{ + // this is awkward due to a zig compiler bug + .has_chain_parent = (e_.optional_chain orelse js_ast.OptionalChain.start) == js_ast.OptionalChain.ccontinue, + }); + e_.target = target; + + if (e_.optional_chain == null and @as(Expr.Tag, e_.index.data) == .e_string) { + if (p.maybeRewritePropertyAccess( + expr.loc, + in.assign_target, + is_delete_target, + e_.target, + p.lexer.utf16ToString(e_.index.data.e_string.value), + e_.index.loc, + is_call_target, + )) |val| { + return val; + } + } + + // Create an error for assigning to an import namespace when bundling. Even + // though this is a run-time error, we make it a compile-time error when + // bundling because scope hoisting means these will no longer be run-time + // errors. + if ((in.assign_target != .none or is_delete_target) and @as(Expr.Tag, e_.target.data) == .e_identifier) { + const r = js_lexer.rangeOfIdentifier(&p.source, e_.target.loc); + p.log.addRangeErrorFmt( + p.source, + r, + p.allocator, + "Cannot assign to property on import \"{s}\"", + .{p.symbols.items[e_.target.data.e_identifier.ref.inner_index].original_name}, + ) catch unreachable; + } + + return p.e(e_, expr.loc); + }, + .e_unary => |e_| { + switch (e_.op) { + .un_typeof => { + e_.value = p.visitExprInOut(e_.value, ExprIn{ .assign_target = e_.op.unaryAssignTarget() }); + + if (SideEffects.toTypeof(e_.value.data)) |typeof| { + return p.e(E.String{ .value = p.lexer.stringToUTF16(typeof) }, expr.loc); + } + }, + .un_delete => { + e_.value = p.visitExprInOut(e_.value, ExprIn{ .has_chain_parent = true }); + }, + else => { + e_.value = p.visitExprInOut(e_.value, ExprIn{ .assign_target = e_.op.unaryAssignTarget() }); + + // Post-process the unary expression + + switch (e_.op) { + .un_not => { + const side_effects = SideEffects.toBoolean(e_.value.data); + if (side_effects.ok) { + return p.e(E.Boolean{ .value = !side_effects.value }, expr.loc); + } + + // maybe won't do this idk + if (Expr.maybeSimplifyNot(&e_.value, p.allocator)) |exp| { + return exp; + } + }, + .un_void => { + if (p.exprCanBeRemovedIfUnused(e_.value)) { + return p.e(E.Undefined{}, e_.value.loc); + } + }, + .un_pos => { + if (SideEffects.toNumber(e_.value.data)) |num| { + return p.e(E.Number{ .value = num }, expr.loc); + } + }, + .un_neg => { + if (SideEffects.toNumber(e_.value.data)) |num| { + return p.e(E.Number{ .value = -num }, expr.loc); + } + }, + + //////////////////////////////////////////////////////////////////////////////// + + .un_pre_dec => { + // TODO: private fields + }, + .un_pre_inc => { + // TODO: private fields + }, + .un_post_dec => { + // TODO: private fields + }, + .un_post_inc => { + // TODO: private fields + }, + else => {}, + } + }, + } }, - .e_identifier => |e_| {}, - .e_private_identifier => |e_| {}, - .e_jsx_element => |e_| {}, + .e_dot => |e_| { + const is_delete_target = e_ == p.delete_target.e_dot; + const is_call_target = e_ == p.call_target.e_dot; + + if (p.define.dots.get(e_.name)) |parts| { + for (parts) |define| { + if (p.isDotDefineMatch(expr, define.parts)) { + // Substitute user-specified defines + if (!define.data.isUndefined()) { + // TODO: check this doesn't crash due to the pointer no longer being allocated + return p.valueForDefine(expr.loc, in.assign_target, is_delete_target, &define.data); + } - .e_template => |e_| {}, + // Copy the side effect flags over in case this expression is unused + if (define.data.can_be_removed_if_unused) { + e_.can_be_removed_if_unused = true; + } + + if (define.data.call_can_be_unwrapped_if_unused and !p.options.ignore_dce_annotations) { + e_.call_can_be_unwrapped_if_unused = true; + } - .e_binary => |e_| {}, - .e_index => |e_| {}, - .e_unary => |e_| {}, - .e_dot => |e_| {}, + break; + } + } + } + }, .e_if => |e_| {}, - .e_await => |e_| {}, - .e_yield => |e_| {}, - .e_array => |e_| {}, - .e_object => |e_| {}, - .e_import => |e_| {}, - .e_call => |e_| {}, - .e_new => |e_| {}, - .e_arrow => |e_| {}, - .e_function => |e_| {}, - .e_class => |e_| {}, + .e_await => |e_| { + notimpl(); + }, + .e_yield => |e_| { + notimpl(); + }, + .e_array => |e_| { + notimpl(); + }, + .e_object => |e_| { + notimpl(); + }, + .e_import => |e_| { + notimpl(); + }, + .e_call => |e_| { + notimpl(); + }, + .e_new => |e_| { + notimpl(); + }, + .e_arrow => |e_| { + notimpl(); + }, + .e_function => |e_| { + notimpl(); + }, + .e_class => |e_| { + notimpl(); + }, else => {}, } return expr; } + pub fn classCanBeRemovedIfUnused(p: *P, class: *G.Class) bool { + if (class.extends) |extends| { + if (!p.exprCanBeRemovedIfUnused(extends)) { + return false; + } + } + + for (class.properties) |property| { + if (!p.exprCanBeRemovedIfUnused(property.key orelse unreachable)) { + return false; + } + + if (property.value) |val| { + if (!p.exprCanBeRemovedIfUnused(val)) { + return false; + } + } + + if (property.initializer) |val| { + if (!p.exprCanBeRemovedIfUnused(val)) { + return false; + } + } + } + + return true; + } + + // TODO: + // When React Fast Refresh is enabled, anything that's a JSX component should not be removable + // This is to improve the reliability of fast refresh between page loads. + pub fn exprCanBeRemovedIfUnused(p: *P, expr: Expr) bool { + switch (expr.data) { + .e_null, + .e_undefined, + .e_missing, + .e_boolean, + .e_number, + .e_big_int, + .e_string, + .e_this, + .e_reg_exp, + .e_function, + .e_arrow, + .e_import_meta, + => { + return true; + }, + + .e_dot => |ex| { + return ex.can_be_removed_if_unused; + }, + .e_class => |ex| { + return p.classCanBeRemovedIfUnused(ex); + }, + .e_identifier => |ex| { + if (ex.must_keep_due_to_with_stmt) { + return false; + } + + // Unbound identifiers cannot be removed because they can have side effects. + // One possible side effect is throwing a ReferenceError if they don't exist. + // Another one is a getter with side effects on the global object: + // + // Object.defineProperty(globalThis, 'x', { + // get() { + // sideEffect(); + // }, + // }); + // + // Be very careful about this possibility. It's tempting to treat all + // identifier expressions as not having side effects but that's wrong. We + // must make sure they have been declared by the code we are currently + // compiling before we can tell that they have no side effects. + // + // Note that we currently ignore ReferenceErrors due to TDZ access. This is + // incorrect but proper TDZ analysis is very complicated and would have to + // be very conservative, which would inhibit a lot of optimizations of code + // inside closures. This may need to be revisited if it proves problematic. + if (ex.can_be_removed_if_unused or p.symbols.items[ex.ref.inner_index].kind != .unbound) { + return true; + } + }, + .e_import_identifier => |ex| { + // References to an ES6 import item are always side-effect free in an + // ECMAScript environment. + // + // They could technically have side effects if the imported module is a + // CommonJS module and the import item was translated to a property access + // (which esbuild's bundler does) and the property has a getter with side + // effects. + // + // But this is very unlikely and respecting this edge case would mean + // disabling tree shaking of all code that references an export from a + // CommonJS module. It would also likely violate the expectations of some + // developers because the code *looks* like it should be able to be tree + // shaken. + // + // So we deliberately ignore this edge case and always treat import item + // references as being side-effect free. + return true; + }, + .e_if => |ex| { + return p.exprCanBeRemovedIfUnused(ex.test_) and p.exprCanBeRemovedIfUnused(ex.yes) and p.exprCanBeRemovedIfUnused(ex.no); + }, + .e_array => |ex| { + for (ex.items) |item| { + if (!p.exprCanBeRemovedIfUnused(item)) { + return false; + } + } + + return true; + }, + .e_object => |ex| { + for (ex.properties) |property| { + + // The key must still be evaluated if it's computed or a spread + if (property.kind == .spread or property.flags.is_computed) { + return false; + } + + if (property.value) |val| { + if (!p.exprCanBeRemovedIfUnused(val)) { + return false; + } + } + } + return true; + }, + .e_call => |ex| { + // A call that has been marked "__PURE__" can be removed if all arguments + // can be removed. The annotation causes us to ignore the target. + if (ex.can_be_unwrapped_if_unused) { + for (ex.args) |arg| { + if (!p.exprCanBeRemovedIfUnused(arg)) { + return false; + } + } + } + + return true; + }, + .e_new => |ex| { + // A call that has been marked "__PURE__" can be removed if all arguments + // can be removed. The annotation causes us to ignore the target. + if (ex.can_be_unwrapped_if_unused) { + for (ex.args) |arg| { + if (!p.exprCanBeRemovedIfUnused(arg)) { + return false; + } + } + } + + return true; + }, + .e_unary => |ex| { + switch (ex.op) { + .un_typeof, .un_void, .un_not => { + return p.exprCanBeRemovedIfUnused(ex.value); + }, + else => {}, + } + }, + .e_binary => |ex| { + switch (ex.op) { + .bin_strict_eq, .bin_strict_ne, .bin_comma, .bin_logical_or, .bin_logical_and, .bin_nullish_coalescing => { + return p.exprCanBeRemovedIfUnused(ex.left) and p.exprCanBeRemovedIfUnused(ex.right); + }, + else => {}, + } + }, + else => {}, + } + + return false; + } + + // EDot nodes represent a property access. This function may return an + // expression to replace the property access with. It assumes that the + // target of the EDot expression has already been visited. + pub fn maybeRewritePropertyAccess( + p: *P, + loc: logger.Loc, + assign_target: js_ast.AssignTarget, + is_delete_target: bool, + target: js_ast.Expr, + name: string, + name_loc: logger.Loc, + is_call_target: bool, + ) ?Expr { + if (@as(Expr.Tag, target.data) == .e_identifier) { + const id = target.data.e_identifier; + + // Rewrite property accesses on explicit namespace imports as an identifier. + // This lets us replace them easily in the printer to rebind them to + // something else without paying the cost of a whole-tree traversal during + // module linking just to rewrite these EDot expressions. + if (p.import_items_for_namespace.get(id.ref)) |*import_items| { + var item: LocRef = undefined; + + if (!import_items.contains(name)) { + item = LocRef{ .loc = name_loc, .ref = p.newSymbol(.import, name) catch unreachable }; + p.module_scope.generated.append(item.ref orelse unreachable) catch unreachable; + + import_items.put(name, item) catch unreachable; + p.is_import_item.put(item.ref orelse unreachable, true) catch unreachable; + + var symbol = p.symbols.items[item.ref.?.inner_index]; + // Mark this as generated in case it's missing. We don't want to + // generate errors for missing import items that are automatically + // generated. + symbol.import_item_status = .generated; + } else { + item = import_items.get(name) orelse unreachable; + } + + // Undo the usage count for the namespace itself. This is used later + // to detect whether the namespace symbol has ever been "captured" + // or whether it has just been used to read properties off of. + // + // The benefit of doing this is that if both this module and the + // imported module end up in the same module group and the namespace + // symbol has never been captured, then we don't need to generate + // any code for the namespace at all. + p.ignoreUsage(id.ref); + + // Track how many times we've referenced this symbol + p.recordUsage(&item.ref.?); + var ident = p.allocator.create(E.Identifier) catch unreachable; + ident.ref = item.ref.?; + + return p.handleIdentifier(name_loc, ident, name, IdentifierOpts{ + .assign_target = assign_target, + .is_delete_target = is_delete_target, + // If this expression is used as the target of a call expression, make + // sure the value of "this" is preserved. + .was_originally_identifier = false, + }); + } + + if (is_call_target and id.ref.eql(p.module_ref) and strings.eql(name, "require")) { + p.ignoreUsage(p.module_ref); + p.recordUsage(&p.require_ref); + return p.e(E.Identifier{ .ref = p.require_ref }, name_loc); + } + + // If this is a known enum value, inline the value of the enum + if (p.options.ts) { + if (p.known_enum_values.get(id.ref)) |enum_value_map| { + if (enum_value_map.get(name)) |enum_value| { + return p.e(E.Number{ .value = enum_value }, loc); + } + } + } + } + + return null; + } + + pub fn ignoreUsage(p: *P, ref: Ref) void { + if (!p.is_control_flow_dead) { + p.symbols.items[ref.inner_index].use_count_estimate = std.math.max(p.symbols.items[ref.inner_index].use_count_estimate - 1, 0); + var use = p.symbol_uses.get(ref) orelse p.panic("Expected symbol_uses to exist {s}\n{s}", .{ ref, p.symbol_uses }); + use.count_estimate = std.math.max(use.count_estimate - 1, 0); + if (use.count_estimate == 0) { + _ = p.symbol_uses.remove(ref); + } else { + p.symbol_uses.putAssumeCapacity(ref, use); + } + } + + // Don't roll back the "tsUseCounts" increment. This must be counted even if + // the value is ignored because that's what the TypeScript compiler does. + } + pub fn visitAndAppendStmt(p: *P, stmts: *List(Stmt), stmt: *Stmt) !void { switch (stmt.data) { // These don't contain anything to traverse @@ -6926,7 +8239,80 @@ pub const P = struct { } pub fn isAnonymousNamedExpr(p: *P, expr: ExprNodeIndex) bool { - notimpl(); + switch (expr.data) { + .e_arrow => { + return true; + }, + .e_function => |func| { + return func.func.name == null; + }, + .e_class => |class| { + return class.class_name == null; + }, + else => { + return false; + }, + } + } + + pub fn valueForDefine(p: *P, loc: logger.Loc, assign_target: js_ast.AssignTarget, is_delete_target: bool, define_data: *const DefineData) Expr { + switch (define_data.value) { + .e_identifier => |ident| { + return p.handleIdentifier( + loc, + ident, + define_data.original_name.?, + IdentifierOpts{ + .assign_target = assign_target, + .is_delete_target = is_delete_target, + .was_originally_identifier = true, + }, + ); + }, + else => {}, + } + + return Expr{ + .data = define_data.value, + .loc = loc, + }; + } + + pub fn isDotDefineMatch(p: *P, expr: Expr, parts: []const string) bool { + switch (expr.data) { + .e_dot => |ex| { + if (parts.len > 1) { + // Intermediates must be dot expressions + const last = parts.len - 1; + return strings.eql(parts[last], ex.name) and ex.optional_chain == null and p.isDotDefineMatch(ex.target, parts[0..last]); + } + }, + .e_import_meta => |ex| { + return parts.len == 2 and strings.eql(parts[0], "import") and strings.eql(parts[1], "meta"); + }, + .e_identifier => |ex| { + // The last expression must be an identifier + if (parts.len == 1) { + const name = p.loadNameFromRef(ex.ref); + if (!strings.eql(name, parts[0])) { + return false; + } + + const result = p.findSymbol(expr.loc, name) catch return false; + + // We must not be in a "with" statement scope + if (result.is_inside_with_scope) { + return false; + } + + // The last symbol must be unbound + return p.symbols.items[result.ref.inner_index].kind == .unbound; + } + }, + else => {}, + } + + return false; } pub fn visitBinding(p: *P, binding: BindingNodeIndex, duplicate_arg_check: ?*StringBoolMap) void { @@ -7318,9 +8704,10 @@ pub const P = struct { return p.e(E.Missing{}, loc); } - pub fn init(allocator: *std.mem.Allocator, log: *logger.Log, source: logger.Source, lexer: js_lexer.Lexer, opts: Parser.Options) !*P { + pub fn init(allocator: *std.mem.Allocator, log: *logger.Log, source: logger.Source, define: *Define, lexer: js_lexer.Lexer, opts: Parser.Options) !*P { var parser = try allocator.create(P); parser.allocated_names = @TypeOf(parser.allocated_names).init(allocator); + parser.define = define; parser.scopes_for_current_part = @TypeOf(parser.scopes_for_current_part).init(allocator); parser.symbols = @TypeOf(parser.symbols).init(allocator); parser.ts_use_counts = @TypeOf(parser.ts_use_counts).init(allocator); @@ -7393,9 +8780,9 @@ fn expectPrintedJS(contents: string, expected: string) !void { var source = logger.Source.initFile(opts.entry_point, alloc.dynamic); var ast: js_ast.Ast = undefined; + var define = try Define.init(alloc.dynamic, null); debugl("INIT PARSER"); - - var parser = try Parser.init(opts, &log, &source, alloc.dynamic); + var parser = try Parser.init(opts, &log, &source, define, alloc.dynamic); debugl("RUN PARSER"); var res = try parser.parse(); diff --git a/src/main.zig b/src/main.zig index 0b8437253..a579243ea 100644 --- a/src/main.zig +++ b/src/main.zig @@ -9,6 +9,7 @@ const js_printer = @import("js_printer.zig"); const js_ast = @import("js_ast.zig"); const linker = @import("linker.zig"); usingnamespace @import("ast/base.zig"); +usingnamespace @import("defines.zig"); pub fn main() anyerror!void { try alloc.setup(std.heap.page_allocator); @@ -31,6 +32,15 @@ pub fn main() anyerror!void { var log = logger.Log.init(alloc.dynamic); var source = logger.Source.initFile(opts.entry_point, alloc.dynamic); var ast: js_ast.Ast = undefined; + var raw_defines = RawDefines.init(alloc.static); + try raw_defines.put("process.env.NODE_ENV", "\"development\""); + + var user_defines = try DefineData.from_input(raw_defines, &log, alloc.static); + + var define = try Define.init( + alloc.static, + user_defines, + ); switch (opts.loader) { .json => { @@ -47,7 +57,7 @@ pub fn main() anyerror!void { ast = js_ast.Ast.initTest(&([_]js_ast.Part{part})); }, .jsx, .tsx, .ts, .js => { - var parser = try js_parser.Parser.init(opts, &log, &source, alloc.dynamic); + var parser = try js_parser.Parser.init(opts, &log, &source, define, alloc.dynamic); var res = try parser.parse(); ast = res.ast; }, diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 9f1ebee87..95b1140c0 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -181,7 +181,7 @@ pub fn toUTF16Buf(in: string, out: []u16) usize { } } - return utf8Iterator.i; + return i; } pub fn toUTF16Alloc(in: string, allocator: *std.mem.Allocator) !JavascriptString { |