diff options
author | 2021-09-30 13:49:46 -0700 | |
---|---|---|
committer | 2021-09-30 13:49:46 -0700 | |
commit | 4cdc8939ab81de83e393d5e52fc2598cfb0bb928 (patch) | |
tree | 1653f745685f780bebc4895b20b48f7d01c9970e | |
parent | ec256209a857390784713609f0f85a54349df380 (diff) | |
download | bun-4cdc8939ab81de83e393d5e52fc2598cfb0bb928.tar.gz bun-4cdc8939ab81de83e393d5e52fc2598cfb0bb928.tar.zst bun-4cdc8939ab81de83e393d5e52fc2598cfb0bb928.zip |
Wire up macro-injected imports and move some structs above the gigantic parser type to reduce bun compile time a little
-rw-r--r-- | src/javascript/jsc/base.zig | 2 | ||||
-rw-r--r-- | src/js_ast.zig | 505 | ||||
-rw-r--r-- | src/js_parser/js_parser.zig | 461 |
3 files changed, 538 insertions, 430 deletions
diff --git a/src/javascript/jsc/base.zig b/src/javascript/jsc/base.zig index b0583dc53..acdb43bda 100644 --- a/src/javascript/jsc/base.zig +++ b/src/javascript/jsc/base.zig @@ -1551,6 +1551,7 @@ pub fn castObj(obj: js.JSObjectRef, comptime Type: type) *Type { } const JSNode = @import("../../js_ast.zig").Macro.JSNode; const LazyPropertiesObject = @import("../../js_ast.zig").Macro.LazyPropertiesObject; +const ModuleNamespace = @import("../../js_ast.zig").Macro.ModuleNamespace; pub const JSPrivateDataPtr = TaggedPointerUnion(.{ ResolveError, BuildError, @@ -1562,6 +1563,7 @@ pub const JSPrivateDataPtr = TaggedPointerUnion(.{ Router, JSNode, LazyPropertiesObject, + ModuleNamespace, }); pub inline fn GetJSPrivateData(comptime Type: type, ref: js.JSObjectRef) ?*Type { diff --git a/src/js_ast.zig b/src/js_ast.zig index a494f879a..56cab7a33 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -2593,6 +2593,9 @@ pub const Expr = struct { e_class, e_require, + // This should never make it to the printer + inline_identifier, + pub fn jsonStringify(self: @This(), opts: anytype, o: anytype) !void { return try std.json.stringify(@tagName(self), opts, o); } @@ -3137,6 +3140,10 @@ pub const Expr = struct { e_new_target: E.NewTarget, e_import_meta: E.ImportMeta, + // This type should not exist outside of MacroContext + // If it ends up in JSParser or JSPrinter, it is a bug. + inline_identifier: i32, + pub const Store = struct { const often = 512; const medium = 256; @@ -4019,6 +4026,8 @@ pub const Macro = struct { caller: Expr, args: []Expr, function_name: string, + comptime Visitor: type, + visitor: *Visitor, ) anyerror!Expr { Expr.Data.Store.disable_reset = true; Stmt.Data.Store.disable_reset = true; @@ -4096,6 +4105,8 @@ pub const Macro = struct { args, source, hash, + comptime Visitor, + visitor, ); // this.macros.getOrPut(key: K) } @@ -4164,6 +4175,11 @@ pub const Macro = struct { .get = JSBindings.getPropertyNodes, .ro = true, }, + + .namespace = .{ + .get = JSBindings.getModuleNamespace, + .ro = true, + }, }, ); @@ -4174,6 +4190,30 @@ pub const Macro = struct { // so it's safe to just avoid that and do it here like this: return JSNode.Class.make(JavaScript.VirtualMachine.vm.global.ref(), ptr); } + + pub fn updateSymbolsMap(this: *JSNode, comptime Visitor: type, visitor: Visitor) void { + switch (this.data) { + Tag.fragment => |frag| { + for (frag) |child| { + if (child.data == .inline_inject) { + child.updateSymbolsMap(Visitor, visitor); + } + } + }, + Tag.inline_inject => |inject| { + for (inject) |child| { + child.updateSymbolsMap(Visitor, visitor); + } + }, + + Tag.s_import => |import| { + visitor.visitImport(import); + }, + + else => {}, + } + } + pub const JSBindings = struct { const getAllocator = JSCBase.getAllocator; @@ -4765,6 +4805,20 @@ pub const Macro = struct { .e_undefined => |value| { return Expr{ .loc = this.loc, .data = .{ .e_undefined = value } }; }, + .fragment => |fragment| { + if (fragment.len == 0) return Expr{ .loc = this.loc, .data = .{ .e_missing = E.Missing{} } }; + + var left = toExpr(fragment[0]); + + if (fragment.len == 1) return left; + + for (fragment[1..]) |item| { + const right = toExpr(item); + left = Expr.joinWithComma(left, right, default_allocator); + } + + return left; + }, else => { return Expr{ .loc = this.loc, .data = .{ .e_missing = .{} } }; }, @@ -4828,6 +4882,7 @@ pub const Macro = struct { g_property: *G.Property, inline_inject: []JSNode, + inline_identifier: i32, pub fn callArgs(this: Data) ExprNodeList { if (this == .e_call) @@ -4889,127 +4944,17 @@ pub const Macro = struct { inline_true, inline_false, inline_inject, + inline_identifier, + fragment, pub const ids: std.EnumArray(Tag, Expr.Data) = brk: { var list = std.EnumArray(Tag, Expr.Data).initFill(Expr.Data{ .e_number = E.Number{ .value = 0.0 } }); - list.set(Tag.e_array, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_array)) }, - }); - list.set(Tag.e_unary, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_unary)) }, - }); - list.set(Tag.e_binary, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_binary)) }, - }); - list.set(Tag.e_boolean, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_boolean)) }, - }); - list.set(Tag.e_super, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_super)) }, - }); - list.set(Tag.e_null, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_null)) }, - }); - list.set(Tag.e_undefined, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_undefined)) }, - }); - list.set(Tag.e_function, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_function)) }, - }); - list.set(Tag.e_new_target, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_new_target)) }, - }); - list.set(Tag.e_import_meta, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_import_meta)) }, - }); - list.set(Tag.e_call, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_call)) }, - }); - list.set(Tag.e_dot, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_dot)) }, - }); - list.set(Tag.e_index, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_index)) }, - }); - list.set(Tag.e_arrow, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_arrow)) }, - }); - list.set(Tag.e_identifier, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_identifier)) }, - }); - list.set(Tag.e_import_identifier, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_import_identifier)) }, - }); - list.set(Tag.e_private_identifier, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_private_identifier)) }, - }); - list.set(Tag.e_jsx_element, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_jsx_element)) }, - }); - list.set(Tag.e_missing, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_missing)) }, - }); - list.set(Tag.e_number, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_number)) }, - }); - list.set(Tag.e_big_int, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_big_int)) }, - }); - list.set(Tag.e_object, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_object)) }, - }); - list.set(Tag.e_spread, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_spread)) }, - }); - list.set(Tag.e_string, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_string)) }, - }); - list.set(Tag.e_template_part, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_template_part)) }, - }); - list.set(Tag.e_template, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_template)) }, - }); - list.set(Tag.e_reg_exp, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_reg_exp)) }, - }); - list.set(Tag.e_await, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_await)) }, - }); - list.set(Tag.e_yield, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_yield)) }, - }); - list.set(Tag.e_if, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_if)) }, - }); - list.set(Tag.e_import, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_import)) }, - }); - list.set(Tag.e_this, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_this)) }, - }); - list.set(Tag.e_class, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_class)) }, - }); - list.set(Tag.e_require, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.e_require)) }, - }); - list.set(Tag.s_import, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.s_import)) }, - }); - list.set(Tag.g_property, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.g_property)) }, - }); - list.set(Tag.s_block, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.s_block)) }, - }); - list.set(Tag.inline_true, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.inline_true)) }, - }); - list.set(Tag.inline_false, Expr.Data{ - .e_number = E.Number{ .value = @intToFloat(f64, @enumToInt(Tag.inline_false)) }, - }); + const fields: []std.builtin.TypeInfo.EnumField = @typeInfo(Tag).Enum.fields; + for (fields) |field| { + list.set(field.value, Expr.Data{ .e_number = E.Number{ .value = @intToFloat(f64, field.value) } }); + } + break :brk list; }; @@ -5023,12 +4968,11 @@ pub const Macro = struct { .{ "undefined", Tag.e_undefined }, .{ "function", Tag.e_function }, .{ "new_target", Tag.e_new_target }, - .{ "import_meta", Tag.e_import_meta }, + .{ "import-meta", Tag.e_import_meta }, .{ "call", Tag.e_call }, .{ "dot", Tag.e_dot }, .{ "index", Tag.e_index }, .{ "arrow", Tag.e_arrow }, - .{ "id", Tag.e_identifier }, .{ "import-id", Tag.e_import_identifier }, .{ "private-id", Tag.e_private_identifier }, .{ "jsx", Tag.e_jsx_element }, @@ -5054,6 +4998,8 @@ pub const Macro = struct { .{ "true", Tag.inline_true }, .{ "false", Tag.inline_false }, .{ "inject", Tag.inline_inject }, + + .{ "id", Tag.inline_identifier }, }); pub const as_expr_tag: std.EnumArray(Tag, Expr.Tag) = brk: { @@ -5198,6 +5144,89 @@ pub const Macro = struct { }; }; + pub const ModuleNamespace = struct { + import_data: ImportData, + + pub const Class = JSCBase.NewClass( + ModuleNamespace, + .{ + .name = "ModuleNamespace", + .read_only = true, + }, + .{ + .getProperty = .{ + .rfn = getProperty, + }, + .hasProperty = .{ + .rfn = hasProperty, + }, + .getPropertyNames = .{ + .rfn = getPropertyNames, + }, + }, + .{}, + ); + + pub fn getProperty( + ctx: js.JSContextRef, + thisObject: js.JSObjectRef, + propertyName: js.JSStringRef, + exception: js.ExceptionRef, + ) callconv(.C) js.JSValueRef { + var this: *ModuleNamespace = JSCBase.GetJSPrivateData(ModuleNamespace, thisObject) orelse return null; + + const len = js.JSStringGetLength(propertyName); + const properties = this.import_data.import.items; + var ptr = js.JSStringGetCharacters8Ptr(propertyName); + var property_slice = ptr[0..len]; + var value_node: JSNode = undefined; + + for (properties) |property| { + if (strings.eql(property.alias, str)) { + const value = property.value orelse return js.JSValueMakeUndefined(ctx); + value_node = JSNode.initExpr(value); + return JSC.JSValue.jsNumberFromInt32(SymbolMap.generateImportHash(property.alias, this.import_data.path)).asRef(); + } + } + + return js.JSValueMakeUndefined(ctx); + } + + pub fn hasProperty( + ctx: js.JSContextRef, + thisObject: js.JSObjectRef, + propertyName: js.JSStringRef, + ) callconv(.C) bool { + var this: *ModuleNamespace = JSCBase.GetJSPrivateData(ModuleNamespace, thisObject) orelse return false; + + const len = js.JSStringGetLength(propertyName); + const properties = this.import_data.import.items; + var ptr = js.JSStringGetCharacters8Ptr(propertyName); + var property_slice = ptr[0..len]; + + for (properties) |property| { + if (strings.eql(property.alias, str)) return true; + } + + return false; + } + + pub fn getPropertyNames( + ctx: js.JSContextRef, + thisObject: js.JSObjectRef, + props: js.JSPropertyNameAccumulatorRef, + ) callconv(.C) void { + var this: *ModuleNamespace = JSCBase.GetJSPrivateData(ModuleNamespace, thisObject) orelse return; + + const items = this.import_data.import.items; + + for (items) |clause| { + const str = clause.alias; + js.JSPropertyNameAccumulatorAddName(props, js.JSStringCreateStatic(str.ptr, str.len)); + } + } + }; + pub const JSNodeList = std.ArrayListUnmanaged(JSNode); pub fn NewJSXWriter(comptime P: type) type { @@ -5666,6 +5695,29 @@ pub const Macro = struct { return true; }, + Tag.inline_identifier => { + // id only accepts "to" and it must be a int32 + self.args.ensureUnusedCapacity(2); + self.args.appendAssumeCapacity(Expr{ .loc = loc, .data = comptime Tag.ids.get(Tag.inline_identifier) }); + + if (propertyValueNamed(props, "to")) |prop| { + switch (prop.data) { + .e_number, .e_if, .e_identifier, .e_import_identifier, .e_index, .e_call, .e_private_identifier, .e_dot, .e_unary, .e_binary => { + self.args.appendAssumeCapacity(p.visitExpr(prop)); + }, + else => { + self.log.addError(self.p.source, prop.loc, "\"to\" prop must be a number", .{}) catch unreachable; + self.args.appendAssumeCapacity(prop); + }, + } + } else { + self.log.addError(self.p.source, prop.loc, "\"to\" prop is required", .{}) catch unreachable; + self.args.appendAssumeCapacity(Expr{ .loc = loc, .data = .{ .e_number = .{ .value = @floatToInt(f64, 0) } } }); + } + + return true; + }, + Tag.s_import => { const default_property_ = propertyValueNamed(props, "default"); const path_property = propertyValueNamed(props, "path") orelse { @@ -5923,15 +5975,6 @@ pub const Macro = struct { } pub const SymbolMap = struct { - // this uses an i32 here instead of a typical Ref - // we only want the final return value's symbols to be added to the symbol map - // the point that the symbol name is referenced may not be when it's added to the symbol map - // this lets you do: - map: std.AutoArrayHashMap(i32, Symbol) = undefined, - allocator: *std.mem.Allocator, - loaded: bool = false, - loc: logger.Loc, - pub fn generateImportHash(name: string, path: string) i32 { var hasher = std.hash.Wyhash.init(8); hasher.update(path); @@ -5939,30 +5982,6 @@ pub const Macro = struct { hasher.update(name); return @bitCast(i32, @truncate(u32, hasher.final())); } - - pub fn putImport(self: *SymbolMap, import: *ImportData) void { - // we use items here - std.debug.assert(import.default_name == null); - - const count = @intCast(u32, @intCast(u32, import.import.items.len)); - - if (!self.loaded) { - self.loaded = true; - self.map = std.AutoArrayHashMap(i32, Symbol).init(self); - } - - self.map.ensureUnusedCapacity(count) catch unreachable; - - for (import.import.items) |clause| { - self.map.putAssumeCapacity( - generateImportHash(clause.alias, import.path), - Symbol{ - .kind = Symbol.Kind.import, - .original_name = clause.name, - }, - ); - } - } }; pub const Writer = struct { @@ -6235,14 +6254,24 @@ pub const Macro = struct { if (!JSLexer.isIdentifier(alias)) throwTypeError(writer.ctx, "import alias must be an identifier", writer.exception); - import.import.items[import_item_i] = ClauseItem{ .alias = alias, .name = name, .alias_loc = writer.loc }; + import.import.items[import_item_i] = ClauseItem{ + .alias = alias, + .original_name = name, + .name = .{ .loc = writer.loc, .ref = Ref.None }, + .alias_loc = writer.loc, + }; import_item_i += 1; } } if (has_default) { - import.import.items[import_item_i] = ClauseItem{ .alias = import_default_name, .name = "default", .alias_loc = writer.loc }; + import.import.items[import_item_i] = ClauseItem{ + .alias = import_default_name, + .name = .{ .loc = writer.loc, .ref = Ref.None }, + .original_name = name, + .alias_loc = writer.loc, + }; import_item_i += 1; } @@ -6426,6 +6455,11 @@ pub const Macro = struct { return true; }, + .inline_identifier => { + const to = (writer.nextJSValue() orelse return false).toInt32(); + expr.* = Expr{ .data = .{ .inline_identifier = to }, .loc = writer.loc }; + return true; + }, else => { return false; @@ -6456,100 +6490,96 @@ pub const Macro = struct { } pub fn writeFromJS(writer: *Writer) ?JSNode { - const out_node: JSNode = brk: { - switch (TagOrJSNode.fromJSValueRef(writer, writer.ctx, (writer.eatArg() orelse return null).asRef())) { - TagOrJSNode.tag => |tag| { - if (tag == Tag.inline_inject) { - const count: u32 = (writer.eatArg() orelse return false).toU32(); - var i: u32 = 0; - while (i < count) : (i += 1) { - const next_value = (writer.eatArg() orelse return null); - const next_value_ref = next_value.asRef(); - if (js.JSValueIsArray(writer.ctx, next_value)) { - const array = next_value; - const array_len = JSC.JSValue.getLengthOfArray(next_value, JavaScript.VirtualMachine.vm.global); - - var array_i: u32 = 0; - while (array_i < array_len) : (array_i += 1) { - var current_value = JSC.JSObject.getIndex(array, JavaScript.VirtualMachine.vm.global, i); - - switch (TagOrJSNode.fromJSValueRef(writer, writer.ctx, current_value.asRef())) { - .node => |node| { - if (node.data != .s_import) { - throwTypeError(writer.ctx, "inject must only contain imports", writer.exception); - return null; - } - writer.inject.append(node); - }, - .tag => |t| { - if (!writer.writeFromJSWithTagInNode(t)) return null; - }, - .invalid => { + switch (TagOrJSNode.fromJSValueRef(writer, writer.ctx, (writer.eatArg() orelse return null).asRef())) { + TagOrJSNode.tag => |tag| { + if (tag == Tag.inline_inject) { + const count: u32 = (writer.eatArg() orelse return false).toU32(); + var i: u32 = 0; + while (i < count) : (i += 1) { + const next_value = (writer.eatArg() orelse return null); + const next_value_ref = next_value.asRef(); + if (js.JSValueIsArray(writer.ctx, next_value)) { + const array = next_value; + const array_len = JSC.JSValue.getLengthOfArray(next_value, JavaScript.VirtualMachine.vm.global); + + var array_i: u32 = 0; + while (array_i < array_len) : (array_i += 1) { + var current_value = JSC.JSObject.getIndex(array, JavaScript.VirtualMachine.vm.global, i); + + switch (TagOrJSNode.fromJSValueRef(writer, writer.ctx, current_value.asRef())) { + .node => |node| { + if (node.data != .s_import) { + throwTypeError(writer.ctx, "inject must only contain imports", writer.exception); return null; - }, - } + } + writer.inject.append(node); + }, + .tag => |t| { + if (!writer.writeFromJSWithTagInNode(t)) return null; + }, + .invalid => { + return null; + }, } - i += 1; - continue; } + i += 1; + continue; } - return JSNode{ .data = .{ .inline_inject = writer.inject.toOwnedSlice() }, .loc = writer.loc }; } + return JSNode{ .data = .{ .inline_inject = writer.inject.toOwnedSlice() }, .loc = writer.loc }; + } - if (tag == Tag.fragment) { - const count: u32 = (writer.eatArg() orelse return null).toU32(); - // collapse single-item fragments - switch (count) { - 0 => { - return JSNode{ .data = .{ .fragment = &[_]JSNode{} }, .loc = writer.loc }; - }, + if (tag == Tag.fragment) { + const count: u32 = (writer.eatArg() orelse return null).toU32(); + // collapse single-item fragments + switch (count) { + 0 => { + return JSNode{ .data = .{ .fragment = &[_]JSNode{} }, .loc = writer.loc }; + }, - 1 => { - var _node = writer.writeFromJS() orelse return null; - while (true) { - switch (_node.data) { - .fragment => |fragment| { - if (fragment.len == 1) { - _node = fragment[0]; - continue; - } - - break :brk _node; - }, - else => { - break :brk _node; - }, - } + 1 => { + var _node = writer.writeFromJS() orelse return null; + while (true) { + switch (_node.data) { + .fragment => |fragment| { + if (fragment.len == 1) { + _node = fragment[0]; + continue; + } + + return _node; + }, + else => { + return _node; + }, } - }, - else => {}, - } - - var i: u32 = 0; - var fragment = std.ArrayList(JSNode).initCapacity(writer.allocator, count) catch return null; - while (i < count) : (i += 1) { - const node = writer.writeFromJS() orelse return null; - fragment.append(node) catch unreachable; - } + } + }, + else => {}, + } - break :brk JSNode{ .data = .{ .fragment = fragment.toOwnedSlice() }, .loc = writer.loc }; + var i: u32 = 0; + var fragment = std.ArrayList(JSNode).initCapacity(writer.allocator, count) catch return null; + while (i < count) : (i += 1) { + const node = writer.writeFromJS() orelse return null; + fragment.append(node) catch unreachable; } - var expr: Expr = Expr{ .loc = writer.loc, .data = .{ .e_null = E.Null{} } }; + return JSNode{ .data = .{ .fragment = fragment.toOwnedSlice() }, .loc = writer.loc }; + } - if (!writer.writeFromJSWithTagInExpr(tag, &expr)) return null; - break :brk JSNode.initExpr(expr); - }, - TagOrJSNode.node => |node| { - break :brk node; - }, - TagOrJSNode.invalid => { - return null; - }, - } - }; + var expr: Expr = Expr{ .loc = writer.loc, .data = .{ .e_null = E.Null{} } }; - if (writer.inject.items.len > 0) {} + if (!writer.writeFromJSWithTagInExpr(tag, &expr)) return null; + return JSNode.initExpr(expr); + }, + TagOrJSNode.node => |node| { + return node; + }, + TagOrJSNode.invalid => { + return null; + }, + } } }; @@ -6817,6 +6847,8 @@ pub const Macro = struct { args: []Expr, source: *const logger.Source, id: i32, + comptime Visitor: type, + visitor: Visitor, ) Expr { if (comptime isDebug) Output.prettyln("<r><d>[macro]<r> call <d><b>{s}<r>", .{function_name}); @@ -6843,7 +6875,8 @@ pub const Macro = struct { const value = promise.result(macro.vm.global.vm()); - if (JSCBase.GetJSPrivateData(JSNode, value.asObjectRef())) |node| { + if (JSCBase.GetJSPrivateData(JSNode, value.asObjectRef())) |*node| { + node.updateSymbolsMap(Visitor, visitor); return node.toExpr(); } else { return Expr{ .data = .{ .e_missing = .{} }, .loc = caller.loc }; diff --git a/src/js_parser/js_parser.zig b/src/js_parser/js_parser.zig index af04478b8..162ffc0ef 100644 --- a/src/js_parser/js_parser.zig +++ b/src/js_parser/js_parser.zig @@ -22,6 +22,31 @@ const JSXFactoryName = "JSX"; const JSXAutomaticName = "jsx_module"; const MacroRefs = std.AutoArrayHashMap(Ref, u32); +// If we are currently in a hoisted child of the module scope, relocate these +// declarations to the top level and return an equivalent assignment statement. +// Make sure to check that the declaration kind is "var" before calling this. +// And make sure to check that the returned statement is not the zero value. +// +// This is done to make some transformations non-destructive +// Without relocating vars to the top level, simplifying this: +// if (false) var foo = 1; +// to nothing is unsafe +// Because "foo" was defined. And now it's not. +pub const RelocateVars = struct { + pub const Mode = enum { normal, for_in_or_for_of }; + + stmt: ?Stmt = null, + ok: bool = false, +}; + +const VisitArgsOpts = struct { + body: []Stmt = &([_]Stmt{}), + has_rest_arg: bool = false, + + // This is true if the function is an arrow function or a method + is_unique_formal_parameters: bool = false, +}; + const BunJSX = struct { pub threadlocal var bun_jsx_identifier: E.Identifier = undefined; }; @@ -65,6 +90,152 @@ pub fn locAfterOp(e: E.Binary) logger.Loc { } const ExportsStringName = "exports"; +const TransposeState = struct { + is_await_target: bool = false, + is_then_catch_target: bool = false, + loc: logger.Loc, +}; + +pub const TypeScript = struct { + // This function is taken from the official TypeScript compiler source code: + // https://github.com/microsoft/TypeScript/blob/master/src/compiler/parser.ts + pub fn canFollowTypeArgumentsInExpression(token: js_lexer.T) bool { + switch (token) { + // These are the only tokens can legally follow a type argument list. So we + // definitely want to treat them as type arg lists. + .t_open_paren, // foo<x>( + .t_no_substitution_template_literal, // foo<T> `...` + // foo<T> `...${100}...` + .t_template_head, + => { + return true; + }, + // These cases can't legally follow a type arg list. However, they're not + // legal expressions either. The user is probably in the middle of a + // generic type. So treat it as such. + .t_dot, // foo<x>. + .t_close_paren, // foo<x>) + .t_close_bracket, // foo<x>] + .t_colon, // foo<x>: + .t_semicolon, // foo<x>; + .t_question, // foo<x>? + .t_equals_equals, // foo<x> == + .t_equals_equals_equals, // foo<x> === + .t_exclamation_equals, // foo<x> != + .t_exclamation_equals_equals, // foo<x> !== + .t_ampersand_ampersand, // foo<x> && + .t_bar_bar, // foo<x> || + .t_question_question, // foo<x> ?? + .t_caret, // foo<x> ^ + .t_ampersand, // foo<x> & + .t_bar, // foo<x> | + .t_close_brace, // foo<x> } + .t_end_of_file, // foo<x> + => { + return true; + }, + + // We don't want to treat these as type arguments. Otherwise we'll parse + // this as an invocation expression. Instead, we want to parse out the + // expression in isolation from the type arguments. + .t_comma, // foo<x>, + .t_open_brace, // foo<x> { + => { + return false; + }, + else => { + // Anything else treat as an expression + return false; + }, + } + } + pub const Identifier = struct { + pub const StmtIdentifier = enum { + s_type, + + s_namespace, + + s_abstract, + + s_module, + + s_interface, + + s_declare, + }; + pub fn forStr(str: string) ?StmtIdentifier { + switch (str.len) { + "type".len => { + return if (std.mem.readIntNative(u32, str[0..4]) == comptime std.mem.readIntNative(u32, "type")) .s_type else null; + }, + "interface".len => { + if (strings.eqlComptime(str, "interface")) { + return .s_interface; + } else if (strings.eqlComptime(str, "namespace")) { + return .s_namespace; + } else { + return null; + } + }, + "abstract".len => { + if (strings.eqlComptime(str, "abstract")) { + return .s_abstract; + } else { + return null; + } + }, + "declare".len => { + if (strings.eqlComptime(str, "declare")) { + return .s_declare; + } else { + return null; + } + }, + "module".len => { + if (strings.eqlComptime(str, "module")) { + return .s_module; + } else { + return null; + } + }, + else => { + return null; + }, + } + } + pub const IMap = std.ComptimeStringMap(Kind, .{ + .{ "unique", .unique }, + .{ "abstract", .abstract }, + .{ "asserts", .asserts }, + .{ "keyof", .prefix }, + .{ "readonly", .prefix }, + .{ "infer", .prefix }, + .{ "any", .primitive }, + .{ "never", .primitive }, + .{ "unknown", .primitive }, + .{ "undefined", .primitive }, + .{ "object", .primitive }, + .{ "number", .primitive }, + .{ "string", .primitive }, + .{ "boolean", .primitive }, + .{ "bigint", .primitive }, + .{ "symbol", .primitive }, + }); + pub const Kind = enum { + normal, + unique, + abstract, + asserts, + prefix, + primitive, + }; + }; + + pub const SkipTypeOptions = struct { + is_return_type: bool = false, + }; +}; + // We must prevent collisions from generated names. // We want to avoid adding a pass over all the symbols in the file. // To do that: @@ -2700,6 +2871,20 @@ const FastRefresh = struct {}; const ImportItemForNamespaceMap = std.StringArrayHashMap(LocRef); +pub const MacroState = struct { + refs: MacroRefs, + prepend_stmts: *List(Stmt) = undefined, + imports: std.AutoArrayHashMap(i32, Ref), + + pub fn init(allocator: *std.mem.Allocator) MacroState { + return MacroState{ + .refs = MacroRefs.init(allocator), + .prepend_stmts = undefined, + .imports = std.AutoArrayHashMap(i32, Ref).init(allocator), + }; + } +}; + pub fn NewParser( comptime js_parser_features: ParserFeatures, ) type { @@ -2709,16 +2894,17 @@ pub fn NewParser( const only_scan_imports_and_do_not_visit = js_parser_features.scan_only; const is_react_fast_refresh_enabled = js_parser_features.react_fast_refresh; - const ImportRecordList = if (only_scan_imports_and_do_not_visit) *std.ArrayList(ImportRecord) else std.ArrayList(ImportRecord); - const NamedImportsType = if (only_scan_imports_and_do_not_visit) *js_ast.Ast.NamedImports else js_ast.Ast.NamedImports; - const NeedsJSXType = if (only_scan_imports_and_do_not_visit) bool else void; - const ParsePassSymbolUsageType = if (only_scan_imports_and_do_not_visit and is_typescript_enabled) *ScanPassResult.ParsePassSymbolUsageMap else void; // P is for Parser! // public only because of Binding.ToExpr return struct { + const ImportRecordList = if (only_scan_imports_and_do_not_visit) *std.ArrayList(ImportRecord) else std.ArrayList(ImportRecord); + const NamedImportsType = if (only_scan_imports_and_do_not_visit) *js_ast.Ast.NamedImports else js_ast.Ast.NamedImports; + const NeedsJSXType = if (only_scan_imports_and_do_not_visit) bool else void; + const ParsePassSymbolUsageType = if (only_scan_imports_and_do_not_visit and is_typescript_enabled) *ScanPassResult.ParsePassSymbolUsageMap else void; + const P = @This(); pub const jsx_transform_type: JSXTransformType = js_parser_jsx; - macro_refs: MacroRefs = undefined, + macro: MacroState = undefined, allocator: *std.mem.Allocator, options: Parser.Options, log: *logger.Log, @@ -2964,12 +3150,6 @@ pub fn NewParser( scope_order_to_visit: []ScopeOrder = &([_]ScopeOrder{}), - const TransposeState = struct { - is_await_target: bool = false, - is_then_catch_target: bool = false, - loc: logger.Loc, - }; - pub fn transposeImport(p: *P, arg: Expr, state: anytype) Expr { // The argument must be a string if (@as(Expr.Tag, arg.data) == .e_string) { @@ -4364,146 +4544,6 @@ pub fn NewParser( return decorators.items; } - pub const TypeScript = struct { - // This function is taken from the official TypeScript compiler source code: - // https://github.com/microsoft/TypeScript/blob/master/src/compiler/parser.ts - pub fn canFollowTypeArgumentsInExpression(p: *P) bool { - switch (p.lexer.token) { - // These are the only tokens can legally follow a type argument list. So we - // definitely want to treat them as type arg lists. - .t_open_paren, // foo<x>( - .t_no_substitution_template_literal, // foo<T> `...` - // foo<T> `...${100}...` - .t_template_head, - => { - return true; - }, - // These cases can't legally follow a type arg list. However, they're not - // legal expressions either. The user is probably in the middle of a - // generic type. So treat it as such. - .t_dot, // foo<x>. - .t_close_paren, // foo<x>) - .t_close_bracket, // foo<x>] - .t_colon, // foo<x>: - .t_semicolon, // foo<x>; - .t_question, // foo<x>? - .t_equals_equals, // foo<x> == - .t_equals_equals_equals, // foo<x> === - .t_exclamation_equals, // foo<x> != - .t_exclamation_equals_equals, // foo<x> !== - .t_ampersand_ampersand, // foo<x> && - .t_bar_bar, // foo<x> || - .t_question_question, // foo<x> ?? - .t_caret, // foo<x> ^ - .t_ampersand, // foo<x> & - .t_bar, // foo<x> | - .t_close_brace, // foo<x> } - .t_end_of_file, // foo<x> - => { - return true; - }, - - // We don't want to treat these as type arguments. Otherwise we'll parse - // this as an invocation expression. Instead, we want to parse out the - // expression in isolation from the type arguments. - .t_comma, // foo<x>, - .t_open_brace, // foo<x> { - => { - return false; - }, - else => { - // Anything else treat as an expression - return false; - }, - } - } - pub const Identifier = struct { - pub const StmtIdentifier = enum { - s_type, - - s_namespace, - - s_abstract, - - s_module, - - s_interface, - - s_declare, - }; - pub fn forStr(str: string) ?StmtIdentifier { - switch (str.len) { - "type".len => { - return if (std.mem.readIntNative(u32, str[0..4]) == std.mem.readIntNative(u32, "type")) .s_type else null; - }, - "interface".len => { - if (strings.eqlComptime(str, "interface")) { - return .s_interface; - } else if (strings.eqlComptime(str, "namespace")) { - return .s_namespace; - } else { - return null; - } - }, - "abstract".len => { - if (strings.eqlComptime(str, "abstract")) { - return .s_abstract; - } else { - return null; - } - }, - "declare".len => { - if (strings.eqlComptime(str, "declare")) { - return .s_declare; - } else { - return null; - } - }, - "module".len => { - if (strings.eqlComptime(str, "module")) { - return .s_module; - } else { - return null; - } - }, - else => { - return null; - }, - } - } - pub const IMap = std.ComptimeStringMap(Kind, .{ - .{ "unique", .unique }, - .{ "abstract", .abstract }, - .{ "asserts", .asserts }, - .{ "keyof", .prefix }, - .{ "readonly", .prefix }, - .{ "infer", .prefix }, - .{ "any", .primitive }, - .{ "never", .primitive }, - .{ "unknown", .primitive }, - .{ "undefined", .primitive }, - .{ "object", .primitive }, - .{ "number", .primitive }, - .{ "string", .primitive }, - .{ "boolean", .primitive }, - .{ "bigint", .primitive }, - .{ "symbol", .primitive }, - }); - pub const Kind = enum { - normal, - unique, - abstract, - asserts, - prefix, - primitive, - }; - }; - - pub const SkipTypeOptions = struct { - is_return_type: bool = false, - }; - }; - fn skipTypeScriptType(p: *P, level: js_ast.Op.Level) anyerror!void { try p.skipTypeScriptTypeWithOpts(level, .{}); } @@ -6224,7 +6264,7 @@ pub fn NewParser( } if (is_macro) { - try p.macro_refs.put(ref, stmt.import_record_index); + try p.macro.refs.put(ref, stmt.import_record_index); } } @@ -6248,7 +6288,7 @@ pub fn NewParser( } if (is_macro) { - try p.macro_refs.put(ref, stmt.import_record_index); + try p.macro.refs.put(ref, stmt.import_record_index); } } } @@ -8012,7 +8052,7 @@ pub fn NewParser( _ = try p.skipTypeScriptTypeArguments(false); // Check the token after this and backtrack if it's the wrong one - if (!TypeScript.canFollowTypeArgumentsInExpression(p)) { + if (!TypeScript.canFollowTypeArgumentsInExpression(p.lexer.token)) { // try p.lexer.unexpected(); return error.SyntaxError; return error.Backtrack; } @@ -9557,6 +9597,35 @@ pub fn NewParser( } } + pub const MacroVisitor = struct { + p: *P, + + loc: logger.Loc, + + pub fn visitImport(this: MacroVisitor, import_data: js_ast.Macro.JSNode.ImportData) void { + var p = this.p; + + const record = p.addImportRecord(.import, import_data.loc, import_data.path); + p.macro.imports.ensureUnusedCapacity(import_data.import.items) catch unreachable; + + var import = import_data.import; + + p.is_import_item.ensureCapacity( + @intCast(u32, p.is_import_item.count() + import.items.len), + ) catch unreachable; + + for (import.items) |*clause| { + const name_ref = p.declareSymbol(.import, this.loc, clause.alias) catch unreachable; + clause.name = LocRef{ .loc = this.loc, .ref = name_ref }; + + p.macro.imports.putAssumeCapacity(js_ast.Macro.JSNode.SymbolMap.generateImportHash(clause.alias, import_data.path), name_ref); + p.is_import_item.put(name_ref, true) catch unreachable; + } + + p.macro.prepend_stmts.append(p.s(import, this.loc)) catch unreachable; + } + }; + pub fn panic(p: *P, comptime str: string, args: anytype) noreturn { @setCold(true); var panic_buffer = p.allocator.alloc(u8, 32 * 1024) catch unreachable; @@ -11186,10 +11255,11 @@ pub fn NewParser( if (comptime FeatureFlags.is_macro_enabled and jsx_transform_type != .macro) { if (e_.tag.?.data == .e_import_identifier) { const ref = e_.tag.?.data.e_import_identifier.ref; - if (p.macro_refs.get(ref)) |import_record_id| { + if (p.macro.refs.get(ref)) |import_record_id| { const name = p.symbols.items[ref.inner_index].original_name; const record = &p.import_records.items[import_record_id]; - return p.options.macro_context.call( + // We must visit it to convert inline_identifiers and record usage + return p.visitExpr(p.options.macro_context.call( record.path.text, p.source.path.sourceDir(), p.log, @@ -11198,13 +11268,31 @@ pub fn NewParser( expr, &.{}, name, - ) catch return expr; + MacroVisitor, + MacroVisitor{ .p = p, .loc = expr.loc }, + ) catch return expr); } } } } }, + .inline_identifier => |id| { + const ref = p.macro.imports.get(id) orelse { + p.panic("Internal error: missing identifier from macro: {d}", .{id}); + }; + + const ident = E.ImportIdentifier{ + .was_originally_identifier = false, + }; + + if (!p.is_control_flow_dead) { + p.recordUsage(ref); + } + + return p.e(ident, expr.loc); + }, + .e_binary => |e_| { switch (e_.left.data) { // Special-case private identifiers @@ -12050,7 +12138,7 @@ pub fn NewParser( const is_macro_ref: bool = if (comptime FeatureFlags.is_macro_enabled and jsx_transform_type != .macro) - e_.target.data == .e_import_identifier and p.macro_refs.contains(e_.target.data.e_import_identifier.ref) + e_.target.data == .e_import_identifier and p.macro.refs.contains(e_.target.data.e_import_identifier.ref) else false; @@ -12081,20 +12169,24 @@ pub fn NewParser( if (comptime FeatureFlags.is_macro_enabled and jsx_transform_type != .macro) { if (is_macro_ref) { const ref = e_.target.data.e_import_identifier.ref; - const import_record_id = p.macro_refs.get(ref).?; + const import_record_id = p.macro.refs.get(ref).?; const name = p.symbols.items[ref.inner_index].original_name; const record = &p.import_records.items[import_record_id]; const copied = Expr{ .loc = expr.loc, .data = .{ .e_call = e_ } }; - return p.options.macro_context.call( - record.path.text, - p.source.path.sourceDir(), - p.log, - p.source, - record.range, - copied, - &.{}, - name, - ) catch return expr; + return p.visitExpr( + p.options.macro_context.call( + record.path.text, + p.source.path.sourceDir(), + p.log, + p.source, + record.range, + copied, + &.{}, + name, + MacroVisitor, + MacroVisitor{ .p = p, .loc = expr.loc }, + ) catch return expr, + ); } } @@ -12159,14 +12251,6 @@ pub fn NewParser( return expr; } - const VisitArgsOpts = struct { - body: []Stmt = &([_]Stmt{}), - has_rest_arg: bool = false, - - // This is true if the function is an arrow function or a method - is_unique_formal_parameters: bool = false, - }; - fn visitArgs(p: *P, args: []G.Arg, opts: VisitArgsOpts) void { const strict_loc = fnBodyContainsUseStrict(opts.body); const has_simple_args = isSimpleParameterList(args, opts.has_rest_arg); @@ -12449,23 +12533,6 @@ pub fn NewParser( return p.jsxStringsToMemberExpression(loc, if (is_static and !p.options.jsx.development) p.jsxs_runtime.ref else p.jsx_runtime.ref); } - // If we are currently in a hoisted child of the module scope, relocate these - // declarations to the top level and return an equivalent assignment statement. - // Make sure to check that the declaration kind is "var" before calling this. - // And make sure to check that the returned statement is not the zero value. - // - // This is done to make some transformations non-destructive - // Without relocating vars to the top level, simplifying this: - // if (false) var foo = 1; - // to nothing is unsafe - // Because "foo" was defined. And now it's not. - pub const RelocateVars = struct { - pub const Mode = enum { normal, for_in_or_for_of }; - - stmt: ?Stmt = null, - ok: bool = false, - }; - fn maybeRelocateVarsToTopLevel(p: *P, decls: []G.Decl, mode: RelocateVars.Mode) RelocateVars { // Only do this when the scope is not already top-level and when we're not inside a function. if (p.current_scope == p.module_scope) { @@ -14125,6 +14192,11 @@ pub fn NewParser( var visited = try List(Stmt).initCapacity(p.allocator, stmts.items.len); var before = List(Stmt).init(p.allocator); var after = List(Stmt).init(p.allocator); + + if (p.current_scope == p.module_scope) { + p.macro.prepend_stmts = &before; + } + defer before.deinit(); defer visited.deinit(); defer after.deinit(); @@ -15322,6 +15394,7 @@ pub fn NewParser( .require_transposer = undefined, .require_resolve_transposer = undefined, .source = source, + .macro = MacroState.init(allocator), .needs_jsx_import = if (comptime only_scan_imports_and_do_not_visit) false else NeedsJSXType{}, .lexer = lexer, |