diff options
| -rw-r--r-- | src/js_ast.zig | 347 | ||||
| -rw-r--r-- | src/js_parser/js_parser.zig | 208 |
2 files changed, 506 insertions, 49 deletions
diff --git a/src/js_ast.zig b/src/js_ast.zig index 35bc67c13..88bf444f0 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -18,9 +18,10 @@ const ObjectPool = @import("./pool.zig").ObjectPool; const ImportRecord = @import("import_record.zig").ImportRecord; const allocators = @import("allocators.zig"); const JSC = @import("javascript_core"); - +const HTTP = @import("http"); const RefCtx = @import("./ast/base.zig").RefCtx; const _hash_map = @import("hash_map.zig"); +const JSONParser = @import("./json_parser.zig"); const StringHashMap = _hash_map.StringHashMap; const AutoHashMap = _hash_map.AutoHashMap; const StringHashMapUnmanaged = _hash_map.StringHashMapUnmanaged; @@ -990,6 +991,7 @@ pub const E = struct { comma_after_spread: ?logger.Loc = null, is_single_line: bool = false, is_parenthesized: bool = false, + was_originally_macro: bool = false, pub fn push(this: *Array, allocator: std.mem.Allocator, item: Expr) !void { try this.items.push(allocator, item); @@ -1289,6 +1291,7 @@ pub const E = struct { comma_after_spread: ?logger.Loc = null, is_single_line: bool = false, is_parenthesized: bool = false, + was_originally_macro: bool = false, pub const Rope = struct { head: Expr, @@ -2115,11 +2118,11 @@ pub const Expr = struct { }; } - pub fn toEmpty(expr: *Expr) Expr { + pub fn toEmpty(expr: Expr) Expr { return Expr{ .data = .{ .e_missing = E.Missing{} }, .loc = expr.loc }; } - pub fn isEmpty(expr: *Expr) bool { - return std.meta.activeTag(expr.data) == .e_missing; + pub fn isEmpty(expr: Expr) bool { + return expr.data == .e_missing; } pub const Query = struct { expr: Expr, loc: logger.Loc, i: u32 = 0 }; @@ -3222,11 +3225,11 @@ pub const Expr = struct { }; } - // The given "expr" argument should be the operand of a "!" prefix operator - // (i.e. the "x" in "!x"). This returns a simplified expression for the - // whole operator (i.e. the "!x") if it can be simplified, or false if not. - // It's separate from "Not()" above to avoid allocation on failure in case - // that is undesired. + /// The given "expr" argument should be the operand of a "!" prefix operator + /// (i.e. the "x" in "!x"). This returns a simplified expression for the + /// whole operator (i.e. the "!x") if it can be simplified, or false if not. + /// It's separate from "Not()" above to avoid allocation on failure in case + /// that is undesired. pub fn maybeSimplifyNot(expr: *Expr, allocator: std.mem.Allocator) ?Expr { switch (expr.data) { .e_null, .e_undefined => { @@ -3412,7 +3415,7 @@ pub const Expr = struct { .bin_nullish_coalescing => { const left = binary.left.data.knownPrimitive(); const right = binary.right.data.knownPrimitive(); - if (left == .@"null" or right == .@"undefined") + if (left == .@"null" or left == .@"undefined") break :brk right; if (left != .unknown) { @@ -4664,6 +4667,8 @@ pub const Macro = struct { pub const JSNode = struct { loc: logger.Loc, data: Data, + visited: bool = false, + pub const Class = JSCBase.NewClass( JSNode, .{ @@ -7475,19 +7480,294 @@ pub const Macro = struct { } pub const Runner = struct { + caller: Expr, + function_name: string, + macro: *const Macro, + allocator: std.mem.Allocator, + id: i32, + log: *logger.Log, + source: *const logger.Source, + is_top_level: bool = true, + visited: VisitMap = VisitMap{}, + + const VisitMap = std.AutoHashMapUnmanaged(JSC.JSValue, Expr); + threadlocal var args_buf: [2]js.JSObjectRef = undefined; threadlocal var expr_nodes_buf: [1]JSNode = undefined; threadlocal var exception_holder: Zig.ZigException.Holder = undefined; pub const MacroError = error{MacroFailed}; + fn coerce( + this: *Runner, + value: JSC.JSValue, + global: *JSC.JSGlobalObject, + comptime Visitor: type, + visitor: Visitor, + ) MacroError!Expr { + if (value.isUndefined()) { + if (this.is_top_level) { + return this.caller; + } + + return Expr.init(E.Undefined, E.Undefined{}, this.caller.loc); + } else if (value.isNull()) { + return Expr.init(E.Null, E.Null{}, this.caller.loc); + } + + this.is_top_level = false; + + if (value.isError() or value.isAggregateError(global) or value.isException(global.vm())) { + this.macro.vm.defaultErrorHandler(value, null); + return error.MacroFailed; + } + + const console_tag = JSC.ZigConsoleClient.Formatter.Tag.get(value, global); + switch (console_tag.tag) { + .Error, .Undefined, .Null => unreachable, + .Private => { + var _entry = this.visited.getOrPut(this.allocator, value) catch unreachable; + if (_entry.found_existing) { + return _entry.value_ptr.*; + } + + if (JSCBase.GetJSPrivateData(JSNode, value.asObjectRef())) |node| { + _entry.value_ptr.* = node.toExpr(); + node.visited = true; + node.updateSymbolsMap(Visitor, visitor); + return _entry.value_ptr.*; + } + + if (JSCBase.GetJSPrivateData(JSC.BuildError, value.asObjectRef()) != null) { + this.macro.vm.defaultErrorHandler(value, null); + return error.MacroFailed; + } + + if (JSCBase.GetJSPrivateData(JSC.ResolveError, value.asObjectRef()) != null) { + this.macro.vm.defaultErrorHandler(value, null); + return error.MacroFailed; + } + + // alright this is insane + if (JSCBase.GetJSPrivateData(JSC.WebCore.Response, value.asObjectRef())) |response| { + switch (response.body.value) { + .Unconsumed => { + if (response.body.len > 0) { + var mime_type = HTTP.MimeType.other; + if (response.body.init.headers) |headers| { + if (headers.getHeaderIndex("content-type")) |content_type| { + mime_type = HTTP.MimeType.init(headers.asStr(headers.entries.get(content_type).value)); + } + } + + if (response.body.ptr) |_ptr| { + var zig_string = JSC.ZigString.init(_ptr[0..response.body.len]); + + if (mime_type.category == .json) { + var source = logger.Source.initPathString("fetch.json", zig_string.slice()); + var out_expr = JSONParser.ParseJSON(&source, this.log, this.allocator) catch { + return error.MacroFailed; + }; + switch (out_expr.data) { + .e_object => { + out_expr.data.e_object.was_originally_macro = true; + }, + .e_array => { + out_expr.data.e_array.was_originally_macro = true; + }, + else => {}, + } + + return out_expr; + } + + if (mime_type.category.isTextLike()) { + zig_string.detectEncoding(); + const utf8 = if (zig_string.is16Bit()) + zig_string.toSlice(this.allocator).slice() + else + zig_string.slice(); + + return Expr.init(E.String, E.String{ .utf8 = utf8 }, this.caller.loc); + } + + return Expr.init(E.String, E.String{ .utf8 = zig_string.toBase64DataURL(this.allocator) catch unreachable }, this.caller.loc); + } + } + + return Expr.init(E.String, E.String.empty, this.caller.loc); + }, + .Empty => { + return Expr.init(E.String, E.String.empty, this.caller.loc); + }, + .String => |str| { + var zig_string = JSC.ZigString.init(str); + + zig_string.detectEncoding(); + if (zig_string.is16Bit()) { + var slice = zig_string.toSlice(this.allocator); + if (response.body.ptr_allocator) |allocator| response.body.deinit(allocator); + return Expr.init(E.String, E.String{ .utf8 = slice.slice() }, this.caller.loc); + } + + return Expr.init(E.String, E.String{ .utf8 = zig_string.slice() }, this.caller.loc); + }, + .ArrayBuffer => |buffer| { + return Expr.init( + E.String, + E.String{ .utf8 = JSC.ZigString.init(buffer.slice()).toBase64DataURL(this.allocator) catch unreachable }, + this.caller.loc, + ); + }, + } + } + }, + + .Boolean => { + return Expr{ .data = .{ .e_boolean = .{ .value = value.toBoolean() } }, .loc = this.caller.loc }; + }, + JSC.ZigConsoleClient.Formatter.Tag.Array => { + var _entry = this.visited.getOrPut(this.allocator, value) catch unreachable; + if (_entry.found_existing) { + return _entry.value_ptr.*; + } + + var iter = JSC.JSArrayIterator.init(value, global); + if (iter.len == 0) { + return Expr.init( + E.Array, + E.Array{ + .items = ExprNodeList.init(&[_]Expr{}), + .was_originally_macro = true, + }, + this.caller.loc, + ); + } + var array = this.allocator.alloc(Expr, iter.len) catch unreachable; + errdefer this.allocator.free(array); + var i: usize = 0; + while (iter.next()) |item| { + array[i] = try this.coerce(item, global, Visitor, visitor); + if (array[i].isMissing()) + continue; + i += 1; + } + + const out = Expr.init( + E.Array, + E.Array{ + .items = ExprNodeList.init(array[0..i]), + .was_originally_macro = true, + }, + this.caller.loc, + ); + _entry.value_ptr.* = out; + return out; + }, + // TODO: optimize this + JSC.ZigConsoleClient.Formatter.Tag.Object => { + var _entry = this.visited.getOrPut(this.allocator, value) catch unreachable; + if (_entry.found_existing) { + return _entry.value_ptr.*; + } + + var object = value.asObjectRef(); + var array = JSC.C.JSObjectCopyPropertyNames(global.ref(), object); + defer JSC.C.JSPropertyNameArrayRelease(array); + const count_ = JSC.C.JSPropertyNameArrayGetCount(array); + var properties = this.allocator.alloc(G.Property, count_) catch unreachable; + errdefer this.allocator.free(properties); + var i: usize = 0; + while (i < count_) : (i += 1) { + var property_name_ref = JSC.C.JSPropertyNameArrayGetNameAtIndex(array, i); + defer JSC.C.JSStringRelease(property_name_ref); + properties[i] = G.Property{ + .key = Expr.init(E.String, E.String{ .utf8 = this.allocator.dupe( + u8, + JSC.C.JSStringGetCharacters8Ptr(property_name_ref)[0..JSC.C.JSStringGetLength(property_name_ref)], + ) catch unreachable }, this.caller.loc), + .value = try this.coerce( + JSC.JSValue.fromRef(JSC.C.JSObjectGetProperty(global.ref(), object, property_name_ref, null)), + global, + Visitor, + visitor, + ), + }; + } + const out = Expr.init( + E.Object, + E.Object{ + .properties = BabyList(G.Property).init(properties[0..i]), + .was_originally_macro = true, + }, + this.caller.loc, + ); + _entry.value_ptr.* = out; + return out; + }, + + .JSON => { + // if (console_tag.cell == .JSDate) { + // // in the code for printing dates, it never exceeds this amount + // var iso_string_buf = this.allocator.alloc(u8, 36) catch unreachable; + // var str = JSC.ZigString.init(""); + // value.jsonStringify(global, 0, &str); + // var out_buf: []const u8 = std.fmt.bufPrint(iso_string_buf, "{}", .{str}) catch ""; + // if (out_buf.len > 2) { + // // trim the quotes + // out_buf = out_buf[1 .. out_buf.len - 1]; + // } + // return Expr.init(E.New, E.New{.target = Expr.init(E.Dot{.target = E}) }) + // } + }, + + .Integer => { + return Expr.init(E.Number, E.Number{ .value = @intToFloat(f64, value.toInt32()) }, this.caller.loc); + }, + .Double => { + return Expr.init(E.Number, E.Number{ .value = value.asNumber() }, this.caller.loc); + }, + .String => { + var zig_str = value.getZigString(global); + zig_str.detectEncoding(); + var sliced = zig_str.toSlice(this.allocator); + return Expr.init(E.String, E.String{ .utf8 = sliced.slice() }, this.caller.loc); + }, + .Promise => { + var _entry = this.visited.getOrPut(this.allocator, value) catch unreachable; + if (_entry.found_existing) { + return _entry.value_ptr.*; + } + + var promise = JSC.JSPromise.resolvedPromise(global, value); + while (promise.status(global.vm()) == .Pending) { + this.macro.vm.tick(); + } + + const result = try this.coerce(promise.result(global.vm()), global, Visitor, visitor); + _entry.value_ptr.* = result; + return result; + }, + else => {}, + } + + this.log.addErrorFmt( + this.source, + this.caller.loc, + this.allocator, + "cannot coerce {s} to Bun's AST. Please return a valid macro using the JSX syntax", + .{@tagName(console_tag.cell)}, + ) catch unreachable; + return error.MacroFailed; + } + pub fn run( macro: Macro, - _: *logger.Log, - _: std.mem.Allocator, + log: *logger.Log, + allocator: std.mem.Allocator, function_name: string, caller: Expr, args: []Expr, - _: *const logger.Source, + source: *const logger.Source, id: i32, comptime Visitor: type, visitor: Visitor, @@ -7505,33 +7785,24 @@ pub const Macro = struct { var macro_callback = macro.vm.macros.get(id) orelse return caller; var result = js.JSObjectCallAsFunctionReturnValueHoldingAPILock(macro.vm.global.ref(), macro_callback, null, args.len + 1, &args_buf); - js.JSValueProtect(macro.vm.global.ref(), result.asRef()); - defer js.JSValueUnprotect(macro.vm.global.ref(), result.asRef()); - var promise = JSC.JSPromise.resolvedPromise(macro.vm.global, result); - _ = macro.vm.tick(); - - while (promise.status(macro.vm.global.vm()) == .Pending) { - macro.vm.tick(); - } - - if (promise.status(macro.vm.global.vm()) == .Rejected) { - macro.vm.defaultErrorHandler(promise.result(macro.vm.global.vm()), null); - return error.MacroFailed; - } - - const value = promise.result(macro.vm.global.vm()); - if (value.isError() or value.isAggregateError(macro.vm.global) or value.isException(macro.vm.global.vm())) { - macro.vm.defaultErrorHandler(value, null); - return error.MacroFailed; - } + var runner = Runner{ + .caller = caller, + .function_name = function_name, + .macro = ¯o, + .allocator = allocator, + .id = id, + .log = log, + .source = source, + }; + defer runner.visited.deinit(allocator); - if (JSCBase.GetJSPrivateData(JSNode, value.asObjectRef())) |node| { - node.updateSymbolsMap(Visitor, visitor); - return node.toExpr(); - } else { - return Expr{ .data = .{ .e_missing = .{} }, .loc = caller.loc }; - } + return try runner.coerce( + result, + macro.vm.global, + Visitor, + visitor, + ); } }; }; diff --git a/src/js_parser/js_parser.zig b/src/js_parser/js_parser.zig index e525c4330..e5add04bf 100644 --- a/src/js_parser/js_parser.zig +++ b/src/js_parser/js_parser.zig @@ -1051,20 +1051,102 @@ pub const SideEffects = enum(u1) { }, .bin_logical_and, .bin_logical_or, .bin_nullish_coalescing => { + bin.right = simpifyUnusedExpr(p, bin.right) orelse bin.right.toEmpty(); // Preserve short-circuit behavior: the left expression is only unused if // the right expression can be completely removed. Otherwise, the left // expression is important for the branch. - if (simpifyUnusedExpr(p, bin.right)) |right| { - bin.right = right; - } else { + + if (bin.right.isEmpty()) return simpifyUnusedExpr(p, bin.left); - } }, else => {}, } }, + .e_object => { + // Arrays with "..." spread expressions can't be unwrapped because the + // "..." triggers code evaluation via iterators. In that case, just trim + // the other items instead and leave the array expression there. + + var properties_slice = expr.data.e_object.properties.slice(); + var end: usize = 0; + var any_computed = false; + for (properties_slice) |spread| { + end = 0; + any_computed = any_computed or spread.flags.is_computed; + if (spread.kind == .spread) { + // Spread properties must always be evaluated + for (properties_slice) |prop_| { + var prop = prop_; + if (prop_.kind != .spread) { + if (prop.value != null) { + if (simpifyUnusedExpr(p, prop.value.?)) |value| { + prop.value = value; + } else if (!prop.flags.is_computed) { + continue; + } else { + prop.value = p.e(E.Number{ .value = 0.0 }, prop.value.?.loc); + } + } + } + + properties_slice[end] = prop_; + end += 1; + } + + properties_slice = properties_slice[0..end]; + expr.data.e_object.properties = G.Property.List.init(properties_slice); + return expr; + } + } + + if (any_computed) { + // Otherwise, the object can be completely removed. We only need to keep any + // object properties with side effects. Apply this simplification recursively. + // for (properties_slice) |prop| { + // if (prop.flags.is_computed) { + // // Make sure "ToString" is still evaluated on the key + + // } + // } + + // keep this for now because we need better test coverage to do this correctly + return expr; + } + + return null; + }, + .e_array => { + var items = expr.data.e_array.items.slice(); + + for (items) |item| { + if (item.data == .e_spread) { + var end: usize = 0; + for (items) |item__| { + var item_ = item__; + if (item_.data != .e_missing) { + items[end] = item_; + end += 1; + } + + expr.data.e_array.items = ExprNodeList.init(items[0..end]); + return expr; + } + } + } + + // Otherwise, the array can be completely removed. We only need to keep any + // array items with side effects. Apply this simplification recursively. + return Expr.joinAllWithCommaCallback( + items, + @TypeOf(p), + p, + simpifyUnusedExpr, + p.allocator, + ); + }, + .e_new => |call| { // A constructor call that has been marked "__PURE__" can be removed if all arguments // can be removed. The annotation causes us to ignore the target. @@ -2909,6 +2991,7 @@ pub fn NewParser( promise_ref: ?Ref = null, scopes_in_order_visitor_index: usize = 0, has_classic_runtime_warned: bool = false, + has_called_macro: bool = false, /// Used for transforming export default -> module.exports has_export_default: bool = false, @@ -11417,6 +11500,7 @@ pub fn NewParser( const ref = e_.tag.?.data.e_import_identifier.ref; if (p.macro.refs.get(ref)) |import_record_id| { const name = p.symbols.items[ref.inner_index].original_name; + p.has_called_macro = true; const record = &p.import_records.items[import_record_id]; // We must visit it to convert inline_identifiers and record usage const macro_result = (p.options.macro_context.call( @@ -11429,7 +11513,10 @@ pub fn NewParser( &.{}, name, MacroVisitor, - MacroVisitor{ .p = p, .loc = expr.loc }, + MacroVisitor{ + .p = p, + .loc = expr.loc, + }, ) catch return expr); if (macro_result.data != .e_template) { @@ -11694,7 +11781,7 @@ pub fn NewParser( // "(null ?? this.fn)" => "this.fn" // "(null ?? this.fn)()" => "(0, this.fn)()" if (is_call_target and e_.right.hasValueForThisInCall()) { - return Expr.joinWithComma(Expr{ .data = Prefill.Data.Zero, .loc = e_.left.loc }, e_.right, p.allocator); + return Expr.joinWithComma(Expr{ .data = .{ .e_number = .{ .value = 0.0 } }, .loc = e_.left.loc }, e_.right, p.allocator); } return e_.right; @@ -11993,10 +12080,9 @@ pub fn NewParser( 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; - // } + if (e_.value.maybeSimplifyNot(p.allocator)) |exp| { + return exp; + } }, .un_void => { if (p.exprCanBeRemovedIfUnused(&e_.value)) { @@ -12088,6 +12174,16 @@ pub fn NewParser( )) |_expr| { return _expr; } + + if (comptime FeatureFlags.is_macro_enabled and jsx_transform_type != .macro) { + if (p.has_called_macro) { + if (e_.target.data == .e_object and e_.target.data.e_object.was_originally_macro) { + if (e_.target.get(e_.name)) |obj| { + return obj; + } + } + } + } } }, .e_if => |e_| { @@ -12342,6 +12438,8 @@ pub fn NewParser( 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_ } }; + const start_error_count = p.log.msgs.items.len; + p.has_called_macro = true; const macro_result = p.options.macro_context.call( record.path.text, @@ -12356,7 +12454,9 @@ pub fn NewParser( MacroVisitor{ .p = p, .loc = expr.loc }, ) catch |err| { if (err == error.MacroFailed) { - p.log.addError(p.source, expr.loc, "macro threw exception") catch unreachable; + if (p.log.msgs.items.len == start_error_count) { + p.log.addError(p.source, expr.loc, "macro threw exception") catch unreachable; + } } else { p.log.addErrorFmt(p.source, expr.loc, p.allocator, "{s} error in macro", .{@errorName(err)}) catch unreachable; } @@ -12862,6 +12962,24 @@ pub fn NewParser( return .{ .stmt = p.s(S.SExpr{ .value = value }, value.loc), .ok = true }; } + // fn maybeInlineMacroObject(p: *P, decl: *G.Decl, macro: Expr) void { + // if (decl.value == null) return; + // switch (decl.binding.data) { + // .b_identifier => |ident| { + // if (macro.get(p.loadNameFromRef(ident.ref))) |val| { + // decl + // } + // } + // } + // } + // if (comptime FeatureFlags.is_macro_enabled and jsx_transform_type != .macro) { + // if (p.has_called_macro and data.decls[i].value != null and + // data.decls[i].value.?.data == .e_object and data.decls[i].value.?.data.e_object.was_originally_macro) + // { + // p.maybeInlineMacroObject(&data.decls[i], data.decls[i].value.?); + // } + // } + // 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. @@ -13232,6 +13350,74 @@ pub fn NewParser( was_anonymous_named_expr, ); }, + .b_object => |bound_object| { + if (comptime FeatureFlags.is_macro_enabled and jsx_transform_type != .macro) { + if (p.has_called_macro and data.decls[i].value != null and + data.decls[i].value.?.data == .e_object and + data.decls[i].value.?.data.e_object.was_originally_macro) + { + bail: { + var object = data.decls[i].value.?.data.e_object; + for (bound_object.properties) |property| { + if (property.flags.is_spread) break :bail; + } + var output_properties = object.properties.slice(); + var end: u32 = 0; + for (bound_object.properties) |property| { + if (property.key.asString(p.allocator)) |name| { + if (object.asProperty(name)) |query| { + output_properties[end] = output_properties[query.i]; + end += 1; + } + } + } + + object.properties.len = end; + } + } + } + }, + .b_array => |bound_array| { + if (comptime FeatureFlags.is_macro_enabled and jsx_transform_type != .macro) { + if (p.has_called_macro and data.decls[i].value != null and + data.decls[i].value.?.data == .e_array and + data.decls[i].value.?.data.e_array.was_originally_macro) + { + bail: { + var array = data.decls[i].value.?.data.e_array; + if (bound_array.has_spread) break :bail; + array.items.len = @minimum(array.items.len, @truncate(u32, bound_array.items.len)); + var slice = array.items.slice(); + outer: for (bound_array.items[0..array.items.len]) |item, item_i| { + const child_expr = slice[item_i]; + switch (item.binding.data) { + .b_object => |bound_object| { + if (child_expr.data != .e_object) continue :outer; + + for (bound_object.properties) |property| { + if (property.flags.is_spread) continue :outer; + } + var object = child_expr.data.e_object; + var output_properties = object.properties.slice(); + var end: u32 = 0; + for (bound_object.properties) |property| { + if (property.key.asString(p.allocator)) |name| { + if (object.asProperty(name)) |query| { + output_properties[end] = output_properties[query.i]; + end += 1; + } + } + } + + object.properties.len = end; + }, + else => {}, + } + } + } + } + } + }, else => {}, } } |
