diff options
Diffstat (limited to 'src/js_parser.zig')
-rw-r--r-- | src/js_parser.zig | 1314 |
1 files changed, 1043 insertions, 271 deletions
diff --git a/src/js_parser.zig b/src/js_parser.zig index c36915e05..b0339e1aa 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -6,6 +6,9 @@ const js_ast = @import("js_ast.zig"); const options = @import("options.zig"); const alloc = @import("alloc.zig"); +const js_printer = @import("js_printer.zig"); +const renamer = @import("renamer.zig"); + const fs = @import("fs.zig"); usingnamespace @import("strings.zig"); usingnamespace @import("ast/base.zig"); @@ -41,6 +44,71 @@ const ExprOrLetStmt = struct { decls: []G.Decl = &([_]G.Decl{}), }; +const FunctionKind = enum { stmt, expr }; + +const EightLetterMatcher = strings.ExactSizeMatcher(8); + +const AsyncPrefixExpression = enum { + none, + is_yield, + is_async, + is_await, + + pub fn find(ident: string) AsyncPrefixExpression { + if (ident.len != 5) { + return .none; + } + + switch (EightLetterMatcher.match(ident)) { + EightLetterMatcher.case("yield") => { + return .is_yield; + }, + EightLetterMatcher.case("await") => { + return .is_await; + }, + EightLetterMatcher.case("async") => { + return .is_async; + }, + + else => { + return .none; + }, + } + } +}; + +fn statementCaresAboutScope(stmt: Stmt) bool { + switch (stmt.data) { + .s_block, + .s_empty, + .s_debugger, + .s_expr, + .s_if, + .s_for, + .s_for_in, + .s_for_of, + .s_do_while, + .s_while, + .s_with, + .s_try, + .s_switch, + .s_return, + .s_throw, + .s_break, + .s_continue, + .s_directive, + => { + return false; + }, + .s_local => |s| { + return s.kind != .k_var; + }, + else => { + return true; + }, + } +} + const ExprIn = struct { // This tells us if there are optional chain expressions (EDot, EIndex, or // ECall) that are chained on to this expression. Because of the way the AST @@ -205,13 +273,19 @@ const ParenExprOpts = struct { force_arrow_fn: bool = false, }; +const AwaitOrYield = enum { + allow_ident, + allow_expr, + forbid_all, +}; + // This is function-specific information used during parsing. It is saved and // restored on the call stack around code that parses nested functions and // arrow expressions. const FnOrArrowDataParse = struct { async_range: ?logger.Range = null, - allow_await: bool = false, - allow_yield: bool = false, + allow_await: AwaitOrYield = AwaitOrYield.allow_ident, + allow_yield: AwaitOrYield = AwaitOrYield.allow_ident, allow_super_call: bool = false, is_top_level: bool = false, is_constructor: bool = false, @@ -225,7 +299,7 @@ const FnOrArrowDataParse = struct { allow_ts_decorators: bool = false, pub fn i() FnOrArrowDataParse { - return FnOrArrowDataParse{ .allow_await = false }; + return FnOrArrowDataParse{ .allow_await = AwaitOrYield.forbid_all }; } }; @@ -352,7 +426,7 @@ pub const Parser = struct { pub const Options = struct { jsx: options.JSX, - ts: bool = true, + ts: bool = false, ascii_only: bool = true, keep_names: bool = true, mangle_syntax: bool = false, @@ -375,8 +449,10 @@ pub const Parser = struct { if (self.p) |p| { // Parse the file in the first pass, but do not bind symbols var opts = ParseStatementOptions{ .is_module_scope = true }; + debugl("<p.parseStmtsUpTo>"); const stmts = try p.parseStmtsUpTo(js_lexer.T.t_end_of_file, &opts); - try p.prepareForVisitPass(); + debugl("</p.parseStmtsUpTo>"); + // try p.prepareForVisitPass(); // ESM is always strict mode. I don't think we need this. // // Strip off a leading "use strict" directive when not bundling @@ -398,18 +474,21 @@ pub const Parser = struct { }, logger.Loc.Empty); } + debugl("<p.appendPart>"); var parts = try List(js_ast.Part).initCapacity(p.allocator, 1); - try p.appendPart(parts, stmts); + try p.appendPart(&parts, stmts); + debugl("</p.appendPart>"); // Pop the module scope to apply the "ContainsDirectEval" rules - p.popScope(); - + // p.popScope(); + debugl("<result.Ast>"); result.ast = js_ast.Ast{ .parts = parts.toOwnedSlice(), .symbols = p.symbols.toOwnedSlice(), - .module_scope = p.module_scope.*, + // .module_scope = p.module_scope.*, }; result.ok = true; + debugl("</result.Ast>"); // result = p.toAST(parts); // result.source_map_comment = p.lexer.source_mapping_url; @@ -422,9 +501,9 @@ pub const Parser = struct { const lexer = try js_lexer.Lexer.init(log, source, allocator); return Parser{ .options = Options{ - .ts = transform.ts, + .ts = transform.loader == .tsx or transform.loader == .ts, .jsx = options.JSX{ - .parse = true, + .parse = transform.loader == .tsx or transform.loader == .jsx, .factory = transform.jsx_factory, .fragment = transform.jsx_fragment, }, @@ -438,6 +517,8 @@ pub const Parser = struct { } }; +const FindLabelSymbolResult = struct { ref: Ref, is_loop: bool, found: bool = false }; + const FindSymbolResult = struct { ref: Ref, declare_loc: ?logger.Loc = null, @@ -472,7 +553,8 @@ const ParseStatementOptions = struct { }; // P is for Parser! -const P = struct { +// public only because of Binding.ToExpr +pub const P = struct { allocator: *std.mem.Allocator, options: Parser.Options, log: *logger.Log, @@ -491,7 +573,7 @@ const P = struct { allocated_names: List(string), latest_arrow_arg_loc: logger.Loc = logger.Loc.Empty, forbid_suffix_after_as_loc: logger.Loc = logger.Loc.Empty, - current_scope: *js_ast.Scope = null, + current_scope: *js_ast.Scope = undefined, scopes_for_current_part: List(*js_ast.Scope), symbols: List(js_ast.Symbol), ts_use_counts: List(u32), @@ -580,10 +662,10 @@ const P = struct { // The visit pass binds identifiers to declared symbols, does constant // folding, substitutes compile-time variable definitions, and lowers certain // syntactic constructs as appropriate. - stmt_expr_value: js_ast.E, - call_target: js_ast.E, - delete_target: js_ast.E, - loop_body: js_ast.S, + stmt_expr_value: Expr.Data, + call_target: Expr.Data, + delete_target: Expr.Data, + loop_body: Stmt.Data, module_scope: *js_ast.Scope = undefined, is_control_flow_dead: bool = false, @@ -647,6 +729,9 @@ const P = struct { // warnings about non-string import paths will be omitted inside try blocks. await_target: ?js_ast.E = null, + to_expr_wrapper_namespace: Binding2ExprWrapper.Namespace, + to_expr_wrapper_hoisted: Binding2ExprWrapper.Hoisted, + // This helps recognize the "import().catch()" pattern. We also try to avoid // warning about this just like the "try { await import() }" pattern. then_catch_chain: ThenCatchChain, @@ -680,6 +765,11 @@ const P = struct { // after_arrow_body_loc: logger.Loc = logger.Loc.Empty, + const Binding2ExprWrapper = struct { + pub const Namespace = Binding.ToExpr(P, P.wrapIdentifierNamespace); + pub const Hoisted = Binding.ToExpr(P, P.wrapIdentifierHoisting); + }; + pub fn s(p: *P, t: anytype, loc: logger.Loc) Stmt { if (@typeInfo(@TypeOf(t)) == .Pointer) { return Stmt.init(t, loc); @@ -932,6 +1022,10 @@ const P = struct { } p.hoistSymbols(p.module_scope); + + p.require_ref = try p.newSymbol(.unbound, "require"); + p.exports_ref = try p.newSymbol(.hoisted, "exports"); + p.module_ref = try p.newSymbol(.hoisted, "module"); } pub fn hoistSymbols(p: *P, scope: *js_ast.Scope) void { @@ -963,7 +1057,7 @@ const P = struct { // Sanity-check that the scopes generated by the first and second passes match if (!order.loc.eql(loc) or order.scope.kind != kind) { - std.debug.panic("Expected scope ({s}, {d}) in {s}, found scope ({s}, {d})", .{ kind, loc.start, p.source.path.pretty, order.scope.kind, order.loc.start }); + p.panic("Expected scope ({s}, {d}) in {s}, found scope ({s}, {d})", .{ kind, loc.start, p.source.path.pretty, order.scope.kind, order.loc.start }); } p.current_scope = order.scope; @@ -972,17 +1066,21 @@ const P = struct { } pub fn pushScopeForParsePass(p: *P, kind: js_ast.Scope.Kind, loc: logger.Loc) !usize { - var parent = p.current_scope; - var scope = try js_ast.Scope.initPtr(p.allocator); + debugl("<pushScopeForParsePass>"); + defer debugl("</pushScopeForParsePass>"); + var scope = try Scope.initPtr(p.allocator); scope.kind = kind; - scope.parent = parent; - scope.label_ref = null; - var i = parent.children.items.len; + var parent: *Scope = undefined; + + if (kind != .entry) { + parent = p.current_scope; + scope.parent = parent; + try parent.children.append(scope); + scope.strict_mode = parent.strict_mode; + } - try parent.children.append(scope); - scope.strict_mode = parent.strict_mode; p.current_scope = scope; // Enforce that scope locations are strictly increasing to help catch bugs @@ -990,7 +1088,7 @@ const P = struct { if (p.scopes_in_order.items.len > 0) { const prev_start = p.scopes_in_order.items[p.scopes_in_order.items.len - 1].loc.start; if (prev_start >= loc.start) { - std.debug.panic("Scope location {d} must be greater than {d}", .{ loc.start, prev_start }); + p.panic("Scope location {d} must be greater than {d}", .{ loc.start, prev_start }); } } @@ -999,7 +1097,7 @@ const P = struct { // arguments. if (kind == js_ast.Scope.Kind.function_body) { if (parent.kind != js_ast.Scope.Kind.function_args) { - std.debug.panic("Internal error", .{}); + p.panic("Internal error", .{}); } var iter = scope.parent.?.members.iterator(); @@ -1013,7 +1111,11 @@ const P = struct { } } - return i; + // Remember the length in case we call popAndDiscardScope() later + const scope_index = p.scopes_in_order.items.len; + try p.scopes_in_order.append(ScopeOrder{ .loc = loc, .scope = scope }); + + return scope_index; } // Note: do not write to "p.log" in this function. Any errors due to conversion @@ -1086,8 +1188,8 @@ const P = struct { .is_computed = item.flags.is_computed, }, - .key = item.key orelse std.debug.panic("Internal error: Expected {s} to have a key.", .{item}), - .value = tup.binding orelse std.debug.panic("Internal error: Expected {s} to have a binding.", .{tup}), + .key = item.key orelse p.panic("Internal error: Expected {s} to have a key.", .{item}), + .value = tup.binding orelse p.panic("Internal error: Expected {s} to have a binding.", .{tup}), .default_value = initializer, }) catch unreachable; } @@ -1209,8 +1311,8 @@ const P = struct { var scopeIndex = try p.pushScopeForParsePass(js_ast.Scope.Kind.function_args, p.lexer.loc()); var func = p.parseFn(name, FnOrArrowDataParse{ .async_range = asyncRange, - .allow_await = isAsync, - .allow_yield = isGenerator, + .allow_await = if (isAsync) AwaitOrYield.allow_expr else AwaitOrYield.allow_ident, + .allow_yield = if (isGenerator) AwaitOrYield.allow_expr else AwaitOrYield.allow_ident, .is_typescript_declare = opts.is_typescript_declare, // Only allow omitting the body if we're parsing TypeScript @@ -1220,8 +1322,38 @@ const P = struct { // Don't output anything if it's just a forward declaration of a function if (opts.is_typescript_declare or func.body == null) { p.popAndDiscardScope(scopeIndex); + + // Balance the fake block scope introduced above + if (hasIfScope) { + p.popScope(); + } + + if (opts.is_typescript_declare and opts.is_namespace_scope and opts.is_export) { + p.has_non_local_export_declare_inside_namespace = true; + } + + return p.s(S.TypeScript{}, loc); } + // Balance the fake block scope introduced above + if (hasIfScope) { + p.popScope(); + } + + // Only declare the function after we know if it had a body or not. Otherwise + // TypeScript code such as this will double-declare the symbol: + // + // function foo(): void; + // function foo(): void {} + // + if (name) |*name_| { + const kind = if (isGenerator or isAsync) Symbol.Kind.generator_or_async_function else Symbol.Kind.hoisted_function; + name_.ref = try p.declareSymbol(kind, name_.loc, nameText); + } + func.name = name; + + func.flags.has_if_scope = hasIfScope; + func.flags.is_export = opts.is_export; return p.s(S.Function{ @@ -1243,7 +1375,7 @@ const P = struct { // Remove the last child from the parent scope var last = children.items.len - 1; if (children.items[last] != to_discard) { - std.debug.panic("Internal error", .{}); + p.panic("Internal error", .{}); } _ = children.popOrNull(); @@ -1258,8 +1390,8 @@ const P = struct { .name = name, .flags = Flags.Function{ .has_rest_arg = false, - .is_async = opts.allow_await, - .is_generator = opts.allow_yield, + .is_async = opts.allow_await == .allow_expr, + .is_generator = opts.allow_yield == .allow_expr, }, .arguments_ref = null, @@ -1269,12 +1401,12 @@ const P = struct { // Await and yield are not allowed in function arguments var old_fn_or_arrow_data = opts; - p.fn_or_arrow_data_parse.allow_await = false; - p.fn_or_arrow_data_parse.allow_yield = false; + p.fn_or_arrow_data_parse.allow_await = if (opts.allow_await == .allow_expr) AwaitOrYield.forbid_all else AwaitOrYield.allow_ident; + p.fn_or_arrow_data_parse.allow_yield = if (opts.allow_yield == .allow_expr) AwaitOrYield.forbid_all else AwaitOrYield.allow_ident; // If "super()" is allowed in the body, it's allowed in the arguments p.fn_or_arrow_data_parse.allow_super_call = opts.allow_super_call; - + var args = List(G.Arg).init(p.allocator); while (p.lexer.token != T.t_close_paren) { // Skip over "this" type annotations if (p.options.ts and p.lexer.token == T.t_this) { @@ -1290,23 +1422,126 @@ const P = struct { p.lexer.next(); continue; } + + var ts_decorators: []ExprNodeIndex = undefined; + if (opts.allow_ts_decorators) { + ts_decorators = p.parseTypeScriptDecorators(); + } + + if (!func.flags.has_rest_arg and p.lexer.token == T.t_dot_dot_dot) { + // p.markSyntaxFeature + p.lexer.next(); + func.flags.has_rest_arg = true; + } + + var is_typescript_ctor_field = false; + var is_identifier = p.lexer.token == T.t_identifier; + var text = p.lexer.identifier; + var arg = p.parseBinding(); + + if (p.options.ts and is_identifier and opts.is_constructor) { + // Skip over TypeScript accessibility modifiers, which turn this argument + // into a class field when used inside a class constructor. This is known + // as a "parameter property" in TypeScript. + while (true) { + switch (p.lexer.token) { + .t_identifier, .t_open_brace, .t_open_bracket => { + if (!js_lexer.TypeScriptAccessibilityModifier.has(p.lexer.identifier)) { + break; + } + + is_typescript_ctor_field = true; + + // TypeScript requires an identifier binding + if (p.lexer.token != .t_identifier) { + p.lexer.expect(.t_identifier); + } + text = p.lexer.identifier; + + // Re-parse the binding (the current binding is the TypeScript keyword) + arg = p.parseBinding(); + }, + else => { + break; + }, + } + } + + // "function foo(a?) {}" + if (p.lexer.token == .t_question) { + p.lexer.next(); + } + + // "function foo(a: any) {}" + if (p.lexer.token == .t_colon) { + p.lexer.next(); + p.skipTypescriptType(.lowest); + } + } + + var parseStmtOpts = ParseStatementOptions{}; + p.declareBinding(.hoisted, arg, &parseStmtOpts) catch unreachable; + + var default_value: Expr = undefined; + if (!func.flags.has_rest_arg and p.lexer.token == .t_equals) { + // p.markSyntaxFeature + p.lexer.next(); + default_value = p.parseExpr(.comma); + } + + args.append(G.Arg{ + .ts_decorators = ts_decorators, + .binding = arg, + .default = default_value, + + // We need to track this because it affects code generation + .is_typescript_ctor_field = is_typescript_ctor_field, + }) catch unreachable; + + if (p.lexer.token != .t_comma) { + break; + } + + if (func.flags.has_rest_arg) { + // JavaScript does not allow a comma after a rest argument + if (opts.is_typescript_declare) { + // TypeScript does allow a comma after a rest argument in a "declare" context + p.lexer.next(); + } else { + p.lexer.expect(.t_close_paren); + } + + break; + } + + p.lexer.next(); } - var ts_decorators: []ExprNodeIndex = undefined; - if (opts.allow_ts_decorators) { - ts_decorators = p.parseTypeScriptDecorators(); + // Reserve the special name "arguments" in this scope. This ensures that it + // shadows any variable called "arguments" in any parent scopes. But only do + // this if it wasn't already declared above because arguments are allowed to + // be called "arguments", in which case the real "arguments" is inaccessible. + if (!p.current_scope.members.contains("arguments")) { + func.arguments_ref = p.declareSymbol(.arguments, func.open_parens_loc, "arguments") catch unreachable; + p.symbols.items[func.arguments_ref.?.inner_index].must_not_be_renamed = true; } - if (!func.flags.has_rest_arg and p.lexer.token == T.t_dot_dot_dot) { - // p.markSyntaxFeature + p.lexer.expect(.t_close_paren); + p.fn_or_arrow_data_parse = old_fn_or_arrow_data; + + // "function foo(): any {}" + if (p.options.ts and p.lexer.token == .t_colon) { p.lexer.next(); - func.flags.has_rest_arg = true; + p.skipTypescriptReturnType(); } - var is_typescript_ctor_field = false; - var is_identifier = p.lexer.token == T.t_identifier; - // TODO: parseFn - // var arg = p.parseBinding(); + // "function foo(): any;" + if (opts.allow_missing_body_for_type_script and p.lexer.token != .t_open_brace) { + p.lexer.expectOrInsertSemicolon(); + return func; + } + var tempOpts = opts; + func.body = p.parseFnBody(&tempOpts) catch unreachable; return func; } @@ -1314,6 +1549,11 @@ const P = struct { // pub fn parseBinding(p: *P) // TODO: + pub fn skipTypescriptReturnType(p: *P) void { + notimpl(); + } + + // TODO: pub fn parseTypeScriptDecorators(p: *P) []ExprNodeIndex { notimpl(); } @@ -1431,12 +1671,12 @@ const P = struct { var loc = p.lexer.loc(); switch (p.lexer.token) { - js_lexer.T.t_semicolon => { + .t_semicolon => { p.lexer.next(); return Stmt.empty(); }, - js_lexer.T.t_export => { + .t_export => { var previousExportKeyword = p.es6_export_keyword; if (opts.is_module_scope) { p.es6_export_keyword = p.lexer.range(); @@ -1607,7 +1847,7 @@ const P = struct { } else {} }, else => { - std.debug.panic("Internal error: unexpected stmt {s}", .{stmt}); + p.panic("Internal error: unexpected stmt {s}", .{stmt}); }, } @@ -1657,7 +1897,7 @@ const P = struct { return p.s(S.ExportDefault{ .default_name = default_name, .value = js_ast.StmtOrExpr{ .stmt = stmt } }, loc); }, else => { - std.debug.panic("internal error: unexpected", .{}); + p.panic("internal error: unexpected", .{}); }, } } @@ -2028,16 +2268,16 @@ const P = struct { var isForAwait = p.lexer.isContextualKeyword("await"); if (isForAwait) { const await_range = p.lexer.range(); - if (!p.fn_or_arrow_data_parse.allow_await) { + if (p.fn_or_arrow_data_parse.allow_await != .allow_expr) { try p.log.addRangeError(p.source, await_range, "Cannot use \"await\" outside an async function"); isForAwait = false; } else { // TODO: improve error handling here // didGenerateError := p.markSyntaxFeature(compat.ForAwait, awaitRange) - // if p.fnOrArrowDataParse.isTopLevel && !didGenerateError { - // p.topLevelAwaitKeyword = awaitRange - // p.markSyntaxFeature(compat.TopLevelAwait, awaitRange) - // } + if (p.fn_or_arrow_data_parse.is_top_level) { + p.top_level_await_keyword = await_range; + // p.markSyntaxFeature(compat.TopLevelAwait, awaitRange) + } } p.lexer.next(); } @@ -2768,18 +3008,18 @@ const P = struct { pub fn requireInitializers(p: *P, decls: []G.Decl) !void { for (decls) |decl| { - if (decl.value) |val| { + if (decl.value == null) { switch (decl.binding.data) { .b_identifier => |ident| { const r = js_lexer.rangeOfIdentifier(&p.source, decl.binding.loc); try p.log.addRangeErrorFmt(p.source, r, p.allocator, "The constant \"{s}\" must be initialized", .{p.symbols.items[ident.ref.inner_index].original_name}); - return; + // return;/ + }, + else => { + try p.log.addError(p.source, decl.binding.loc, "This constant must be initialized"); }, - else => {}, } } - - try p.log.addError(p.source, decl.binding.loc, "This constant must be initialized"); } } @@ -2789,7 +3029,7 @@ const P = struct { switch (p.lexer.token) { .t_identifier => { const name = p.lexer.identifier; - if ((p.fn_or_arrow_data_parse.allow_await and strings.eql(name, "await")) or (p.fn_or_arrow_data_parse.allow_yield and strings.eql(name, "yield"))) { + if ((p.fn_or_arrow_data_parse.allow_await != .allow_ident and strings.eql(name, "await")) or (p.fn_or_arrow_data_parse.allow_yield != .allow_ident and strings.eql(name, "yield"))) { // TODO: add fmt to addRangeError p.log.addRangeError(p.source, p.lexer.range(), "Cannot use \"yield\" or \"await\" here.") catch unreachable; } @@ -3183,7 +3423,7 @@ const P = struct { }, p.lexer.loc())); } - if (p.lexer.token == .t_end_of_file) { + if (p.lexer.token == eend) { break :run; } @@ -3386,6 +3626,26 @@ const P = struct { return ref; } + pub fn validateFunctionName(p: *P, func: G.Fn, kind: FunctionKind) void { + if (func.name) |name| { + const original_name = p.symbols.items[name.ref.?.inner_index].original_name; + + if (func.flags.is_async and strings.eql(original_name, "await")) { + p.log.addRangeError( + p.source, + js_lexer.rangeOfIdentifier(&p.source, name.loc), + "An async function cannot be named \"await\"", + ) catch unreachable; + } else if (kind == .expr and func.flags.is_generator and strings.eql(original_name, "yield")) { + p.log.addRangeError( + p.source, + js_lexer.rangeOfIdentifier(&p.source, name.loc), + "An generator function expression cannot be named \"yield\"", + ) catch unreachable; + } + } + } + pub fn parseFnExpr(p: *P, loc: logger.Loc, is_async: bool, async_range: logger.Range) !Expr { p.lexer.next(); const is_generator = p.lexer.token == T.t_asterisk; @@ -3412,6 +3672,7 @@ const P = struct { } else { (name orelse unreachable).ref = try p.newSymbol(.hoisted_function, p.lexer.identifier); } + debug("FUNC NAME {s}", .{p.lexer.identifier}); p.lexer.next(); } @@ -3421,10 +3682,12 @@ const P = struct { var func = p.parseFn(name, FnOrArrowDataParse{ .async_range = async_range, - .allow_await = is_async, - .allow_yield = is_generator, + .allow_await = if (is_async) .allow_expr else .allow_ident, + .allow_yield = if (is_generator) .allow_expr else .allow_ident, }); + p.validateFunctionName(func, .expr); + return p.e(js_ast.E.Function{ .func = func, }, loc); @@ -3549,74 +3812,23 @@ const P = struct { return self.mm(@TypeOf(kind), kind); } - // The name is temporarily stored in the ref until the scope traversal pass - // happens, at which point a symbol will be generated and the ref will point - // to the symbol instead. - // - // The scope traversal pass will reconstruct the name using one of two methods. - // In the common case, the name is a slice of the file itself. In that case we - // can just store the slice and not need to allocate any extra memory. In the - // rare case, the name is an externally-allocated string. In that case we store - // an index to the string and use that index during the scope traversal pass. + // Doing this the fast way is too complicated for now. pub fn storeNameInRef(p: *P, name: string) !js_ast.Ref { - // jarred: honestly, this is kind of magic to me - // but I think I think I understand it. - // the strings are slices. - // "name" is just a different place in p.source.contents's buffer - // Instead of copying a shit ton of strings everywhere - // we can just say "yeah this is really over here at inner_index" - // .source_index being null is used to identify was this allocated or is just in the orignial thing. - // you could never do this in JavaScript!! - const ptr0 = @ptrToInt(name.ptr); - const ptr1 = @ptrToInt(p.source.contents.ptr); - - // Is the data in "name" a subset of the data in "p.source.Contents"? - if (ptr0 >= ptr1 and ptr0 + name.len < p.source.contents.len) { - // std.debug.print("storeNameInRef fast path", .{}); - // The name is a slice of the file contents, so we can just reference it by - // length and don't have to allocate anything. This is the common case. - // - // It's stored as a negative value so we'll crash if we try to use it. That - // way we'll catch cases where we've forgotten to call loadNameFromRef(). - // The length is the negative part because we know it's non-zero. - return js_ast.Ref{ .source_index = @intCast(Ref.Int, ptr0), .inner_index = (@intCast(Ref.Int, name.len) + @intCast(Ref.Int, ptr0)) }; + // allocated_names is lazily allocated + if (p.allocated_names.capacity > 0) { + const inner_index = @intCast(Ref.Int, p.allocated_names.items.len); + try p.allocated_names.append(name); + return js_ast.Ref{ .source_index = std.math.maxInt(Ref.Int), .inner_index = inner_index }; } else { - // std.debug.print("storeNameInRef slow path", .{}); - // The name is some memory allocated elsewhere. This is either an inline - // string constant in the parser or an identifier with escape sequences - // in the source code, which is very unusual. Stash it away for later. - // This uses allocations but it should hopefully be very uncommon. - - // allocated_names is lazily allocated - if (p.allocated_names.capacity > 0) { - const inner_index = @intCast(Ref.Int, p.allocated_names.items.len); - try p.allocated_names.append(name); - return js_ast.Ref{ .source_index = std.math.maxInt(Ref.Int), .inner_index = inner_index }; - } else { - p.allocated_names = try @TypeOf(p.allocated_names).initCapacity(p.allocator, 1); - p.allocated_names.appendAssumeCapacity(name); - return js_ast.Ref{ .source_index = std.math.maxInt(Ref.Int), .inner_index = 0 }; - } - - // p.allocatedNames = append(p.allocatedNames, name) - // return ref + p.allocated_names = try @TypeOf(p.allocated_names).initCapacity(p.allocator, 1); + p.allocated_names.appendAssumeCapacity(name); + return js_ast.Ref{ .source_index = std.math.maxInt(Ref.Int), .inner_index = 0 }; } } pub fn loadNameFromRef(p: *P, ref: js_ast.Ref) string { - if (!ref.isSourceNull()) { - if (ref.source_index == 0x80000000) { - return p.allocated_names.items[ref.inner_index]; - } - - if (std.builtin.mode != std.builtin.Mode.ReleaseFast) { - assert(ref.inner_index - ref.source_index > 0); - } - - return p.source.contents[ref.inner_index .. ref.inner_index - ref.source_index]; - } else { - std.debug.panic("Internal error: invalid symbol reference. {s}", .{ref}); - } + assert(ref.inner_index < p.allocated_names.items.len); + return p.allocated_names.items[ref.inner_index]; } // This parses an expression. This assumes we've already parsed the "async" @@ -3634,34 +3846,38 @@ const P = struct { switch (p.lexer.token) { // "async => {}" .t_equals_greater_than => { - const arg = G.Arg{ .binding = p.b( - B.Identifier{ - .ref = try p.storeNameInRef("async"), - }, - async_range.loc, - ) }; - _ = p.pushScopeForParsePass(.function_args, async_range.loc) catch unreachable; - defer p.popScope(); - var arrow_body = try p.parseArrowBodySingleArg(arg, FnOrArrowDataParse{}); - return p.e(arrow_body, async_range.loc); + if (level.lte(.assign)) { + const arg = G.Arg{ .binding = p.b( + B.Identifier{ + .ref = try p.storeNameInRef("async"), + }, + async_range.loc, + ) }; + _ = p.pushScopeForParsePass(.function_args, async_range.loc) catch unreachable; + defer p.popScope(); + var arrow_body = try p.parseArrowBodySingleArg(arg, FnOrArrowDataParse{}); + return p.e(arrow_body, async_range.loc); + } }, // "async x => {}" .t_identifier => { - // p.markLoweredSyntaxFeature(); - const ref = try p.storeNameInRef(p.lexer.identifier); - var arg = G.Arg{ .binding = p.b(B.Identifier{ - .ref = ref, - }, p.lexer.loc()) }; - p.lexer.next(); + if (level.lte(.assign)) { + // p.markLoweredSyntaxFeature(); + const ref = try p.storeNameInRef(p.lexer.identifier); + var arg = G.Arg{ .binding = p.b(B.Identifier{ + .ref = ref, + }, p.lexer.loc()) }; + p.lexer.next(); - _ = try p.pushScopeForParsePass(.function_args, async_range.loc); - defer p.popScope(); + _ = try p.pushScopeForParsePass(.function_args, async_range.loc); + defer p.popScope(); - var arrowBody = try p.parseArrowBodySingleArg(arg, FnOrArrowDataParse{ - .allow_await = true, - }); - arrowBody.is_async = true; - return p.e(arrowBody, async_range.loc); + var arrowBody = try p.parseArrowBodySingleArg(arg, FnOrArrowDataParse{ + .allow_await = .allow_expr, + }); + arrowBody.is_async = true; + return p.e(arrowBody, async_range.loc); + } }, // "async()" @@ -3800,7 +4016,7 @@ const P = struct { } } - p.current_scope = current_scope.parent orelse std.debug.panic("Internal error: attempted to call popScope() on the topmost scope", .{}); + p.current_scope = current_scope.parent orelse p.panic("Internal error: attempted to call popScope() on the topmost scope", .{}); } pub fn markExprAsParenthesized(p: *P, expr: *Expr) void { @@ -3985,7 +4201,7 @@ const P = struct { // Parse a shorthand property if (!opts.is_class and kind == .normal and p.lexer.token != .t_colon and p.lexer.token != .t_open_paren and p.lexer.token != .t_less_than and !opts.is_generator and !js_lexer.Keywords.has(name)) { - if ((p.fn_or_arrow_data_parse.allow_await and strings.eql(name, "await")) or (p.fn_or_arrow_data_parse.allow_yield and strings.eql(name, "yield"))) { + if ((p.fn_or_arrow_data_parse.allow_await != .allow_ident and strings.eql(name, "await")) or (p.fn_or_arrow_data_parse.allow_yield != .allow_ident and strings.eql(name, "yield"))) { // TODO: add fmt to addRangeError p.log.addRangeError(p.source, name_range, "Cannot use \"yield\" or \"await\" here.") catch unreachable; } @@ -4121,8 +4337,8 @@ const P = struct { var func = p.parseFn(null, FnOrArrowDataParse{ .async_range = opts.async_range, - .allow_await = opts.is_async, - .allow_yield = opts.is_generator, + .allow_await = if (opts.is_async) AwaitOrYield.allow_expr else AwaitOrYield.allow_ident, + .allow_yield = if (opts.is_generator) AwaitOrYield.allow_expr else AwaitOrYield.allow_ident, .allow_super_call = opts.class_has_extends and is_constructor, .allow_ts_decorators = opts.allow_ts_decorators, .is_constructor = is_constructor, @@ -4299,7 +4515,7 @@ const P = struct { // Forbid decorators on class constructors if (opts.ts_decorators.len > 0) { - switch ((property.key orelse std.debug.panic("Internal error: Expected property {s} to have a key.", .{property})).data) { + switch ((property.key orelse p.panic("Internal error: Expected property {s} to have a key.", .{property})).data) { .e_string => |str| { if (strings.eqlUtf16("constructor", str.value)) { p.log.addError(p.source, first_decorator_loc, "TypeScript does not allow decorators on class constructors") catch unreachable; @@ -5183,6 +5399,17 @@ const P = struct { } } } + + pub fn panic(p: *P, comptime str: string, args: anytype) noreturn { + p.log.addRangeErrorFmt(p.source, p.lexer.range(), p.allocator, str, args) catch unreachable; + + var fixedBuffer = [_]u8{0} ** 4096; + var stream = std.io.fixedBufferStream(&fixedBuffer); + + p.log.print(stream.writer()) catch unreachable; + std.debug.panic("{s}", .{fixedBuffer}); + } + pub fn _parsePrefix(p: *P, level: Level, errors: *DeferredErrors, flags: Expr.EFlags) Expr { const loc = p.lexer.loc(); const l = @enumToInt(level); @@ -5247,81 +5474,101 @@ const P = struct { p.lexer.next(); // Handle async and await expressions - if (name.len == 5) { - if (strings.eql(name, "async")) { - if (strings.eql(raw, "async")) { + switch (AsyncPrefixExpression.find(name)) { + .is_async => { + if (AsyncPrefixExpression.find(raw) != .is_async) { return p.parseAsyncPrefixExpr(name_range, level) catch unreachable; } - } else if (strings.eql(name, "await")) { - if (p.fn_or_arrow_data_parse.allow_await) { - if (!strings.eql(raw, "await")) { - p.log.addRangeError(p.source, name_range, "The keyword \"await\" cannot be escaped.") catch unreachable; - } else { - if (p.fn_or_arrow_data_parse.is_top_level) { - p.top_level_await_keyword = name_range; - // p.markSyntaxFeature() - } + }, - if (p.fn_or_arrow_data_parse.arrow_arg_errors) |*err| { - err.invalid_expr_await = name_range; + .is_await => { + switch (p.fn_or_arrow_data_parse.allow_await) { + .forbid_all => { + p.log.addRangeError(p.source, name_range, "The keyword \"await\" cannot be used here.") catch unreachable; + }, + .allow_expr => { + if (AsyncPrefixExpression.find(raw) != .is_await) { + p.log.addRangeError(p.source, name_range, "The keyword \"await\" cannot be escaped.") catch unreachable; } else { - p.fn_or_arrow_data_parse.arrow_arg_errors = DeferredArrowArgErrors{ .invalid_expr_await = name_range }; - } + if (p.fn_or_arrow_data_parse.is_top_level) { + p.top_level_await_keyword = name_range; + } - var value = p.parseExpr(.prefix); - if (p.lexer.token == T.t_asterisk_asterisk) { - p.lexer.unexpected(); - } + if (p.fn_or_arrow_data_parse.arrow_arg_errors) |*args| { + args.invalid_expr_await = name_range; + } - return p.e(E.Await{ .value = value }, loc); - } - } - } else if (strings.eql(name, "yield")) { - if (p.fn_or_arrow_data_parse.allow_yield) { - if (strings.eql(raw, "yield")) { - p.log.addRangeError(p.source, name_range, "The keyword \"yield\" cannot be escaped") catch unreachable; - } else { - if (l > @enumToInt(Level.assign)) { - p.log.addRangeError(p.source, name_range, "Cannot use a \"yield\" here without parentheses") catch unreachable; - } + const value = p.parseExpr(.prefix); + if (p.lexer.token == T.t_asterisk_asterisk) { + p.lexer.unexpected(); + } - if (p.fn_or_arrow_data_parse.arrow_arg_errors) |*err| { - err.invalid_expr_yield = name_range; + return p.e(E.Await{ .value = value }, loc); } + }, + .allow_ident => {}, + } + }, - return p.parseYieldExpr(loc); - } - } else if (!p.lexer.has_newline_before) { - // Try to gracefully recover if "yield" is used in the wrong place + .is_yield => { + switch (p.fn_or_arrow_data_parse.allow_yield) { + .forbid_all => { + p.log.addRangeError(p.source, name_range, "The keyword \"yield\" cannot be used here") catch unreachable; + }, + .allow_expr => { + if (AsyncPrefixExpression.find(raw) != .is_yield) { + p.log.addRangeError(p.source, name_range, "The keyword \"yield\" cannot be escaped") catch unreachable; + } else { + if (level.gte(.assign)) { + p.log.addRangeError(p.source, name_range, "Cannot use a \"yield\" here without parentheses") catch unreachable; + } + const value = p.parseExpr(.prefix); - switch (p.lexer.token) { - .t_null, .t_identifier, .t_false, .t_true, .t_numeric_literal, .t_big_integer_literal, .t_string_literal => { - p.log.addRangeError(p.source, name_range, "Cannot use \"yield\" outside a generator function") catch unreachable; - }, - else => {}, - } - } - } + if (p.fn_or_arrow_data_parse.arrow_arg_errors) |*args| { + args.invalid_expr_yield = name_range; + } - // Handle the start of an arrow expression - if (p.lexer.token == .t_equals_greater_than) { - const ref = p.storeNameInRef(name) catch unreachable; - var args = p.allocator.alloc(Arg, 1) catch unreachable; - args[0] = Arg{ .binding = p.b(B.Identifier{ - .ref = ref, - }, loc) }; + if (p.lexer.token == T.t_asterisk_asterisk) { + p.lexer.unexpected(); + } - _ = p.pushScopeForParsePass(.function_args, loc) catch unreachable; - defer p.popScope(); - return p.e(p.parseArrowBody(args, p.m(FnOrArrowDataParse{})) catch unreachable, loc); - } + return p.e(E.Yield{ .value = value }, loc); + } + }, + .allow_ident => { + // Try to gracefully recover if "yield" is used in the wrong place + if (!p.lexer.has_newline_before) { + switch (p.lexer.token) { + .t_null, .t_identifier, .t_false, .t_true, .t_numeric_literal, .t_big_integer_literal, .t_string_literal => { + p.log.addRangeError(p.source, name_range, "Cannot use \"yield\" outside a generator function") catch unreachable; + }, + else => {}, + } + } + }, + } + }, + .none => {}, + } + // Handle the start of an arrow expression + if (p.lexer.token == .t_equals_greater_than) { const ref = p.storeNameInRef(name) catch unreachable; - - return p.e(E.Identifier{ + var args = p.allocator.alloc(Arg, 1) catch unreachable; + args[0] = Arg{ .binding = p.b(B.Identifier{ .ref = ref, - }, loc); + }, loc) }; + + _ = p.pushScopeForParsePass(.function_args, loc) catch unreachable; + defer p.popScope(); + return p.e(p.parseArrowBody(args, p.m(FnOrArrowDataParse{})) catch unreachable, loc); } + + const ref = p.storeNameInRef(name) catch unreachable; + + return p.e(E.Identifier{ + .ref = ref, + }, loc); }, .t_string_literal, .t_no_substitution_template_literal => { return p.parseStringLiteral(); @@ -5784,7 +6031,7 @@ const P = struct { return p._parsePrefix(level, errors orelse &DeferredErrors.None, flags); } - pub fn appendPart(p: *P, parts: List(js_ast.Part), stmts: []Stmt) !void { + pub fn appendPart(p: *P, parts: *List(js_ast.Part), stmts: []Stmt) !void { p.symbol_uses = SymbolUseMap.init(p.allocator); p.declared_symbols.deinit(); p.import_records_for_current_part.deinit(); @@ -5803,7 +6050,10 @@ const P = struct { // const link = p.symbols.items[local.ref.inner_index].link; // } } - // TODO: here + // // TODO: here + try parts.append(js_ast.Part{ + .stmts = stmts, + }); } pub fn visitStmtsAndPrependTempRefs(p: *P, stmts: *List(Stmt), opts: *PrependTempRefsOpts) !void { @@ -5821,7 +6071,7 @@ const P = struct { if (p.fn_only_data_visit.this_capture_ref) |ref| { try p.temp_refs_to_declare.append(TempRef{ .ref = ref, - .value = p.e(E.This{}, opts.fn_body_loc orelse std.debug.panic("Internal error: Expected opts.fn_body_loc to exist", .{})), + .value = p.e(E.This{}, opts.fn_body_loc orelse p.panic("Internal error: Expected opts.fn_body_loc to exist", .{})), }); } } @@ -5838,6 +6088,14 @@ const P = struct { return p.visitExprInOut(expr, ExprIn{}); } + pub fn visitFunc(p: *P, func: *G.Fn, open_parens_loc: logger.Loc) void {} + + pub fn maybeKeepExprSymbolName(p: *P, expr: Expr, original_name: string, was_anonymous_named_expr: bool) Expr { + notimpl(); + + // return expr; + } + pub fn valueForThis(p: *P, loc: logger.Loc) ?Expr { // Substitute "this" if we're inside a static class property initializer if (p.fn_only_data_visit.this_class_static_ref) |*ref| { @@ -5885,6 +6143,7 @@ const P = struct { // return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EIdentifier{Ref: p.captureThis()}}, exprOut{} // } }, + .e_import_meta => |exp| {}, .e_spread => |exp| { return p.visitExpr(exp.value); @@ -5892,7 +6151,9 @@ const P = struct { .e_identifier => |e_| {}, .e_private_identifier => |e_| {}, .e_jsx_element => |e_| {}, + .e_template => |e_| {}, + .e_binary => |e_| {}, .e_index => |e_| {}, .e_unary => |e_| {}, @@ -6015,61 +6276,531 @@ const P = struct { switch (data.value) { .expr => |*expr| { const was_anonymous_named_expr = expr.isAnonymousNamed(); - // data.value.expr = p.m(p.visitExpr(expr.*)); + data.value.expr = p.visitExpr(expr.*); // // Optionally preserve the name - // data.value.expr = p.maybeKeepExprSymbolName(expr, "default", was_anonymous_named_expr); - - // // Discard type-only export default statements - // if (p.options.ts) { - // switch (expr.data) { - // .e_identifier => |ident| { - // const symbol = p.symbols.items[ident.ref.inner_index]; - // if (symbol.kind == .unbound) { - // if (p.local_type_names.get(symbol.original_name)) |local_type| { - // if (local_type.value) { - // return; - // } - // } - // } - // }, - // else => {}, - // } - // } + data.value.expr = p.maybeKeepExprSymbolName(expr.*, "default", was_anonymous_named_expr); + + // Discard type-only export default statements + if (p.options.ts) { + switch (expr.data) { + .e_identifier => |ident| { + const symbol = p.symbols.items[ident.ref.inner_index]; + if (symbol.kind == .unbound) { + if (p.local_type_names.get(symbol.original_name)) |local_type| { + if (local_type) { + return; + } + } + } + }, + else => {}, + } + } }, - .stmt => |st| {}, - } - }, - .s_export_equals => |data| {}, - .s_break => |data| {}, - .s_continue => |data| {}, - .s_label => |data| {}, - .s_local => |data| {}, - .s_expr => |data| {}, - .s_throw => |data| {}, - .s_return => |data| {}, - .s_block => |data| {}, - .s_with => |data| {}, - .s_while => |data| {}, - .s_do_while => |data| {}, - .s_if => |data| {}, - .s_for => |data| {}, - .s_for_in => |data| {}, - .s_for_of => |data| {}, - .s_try => |data| {}, - .s_switch => |data| {}, - .s_function => |data| {}, - .s_class => |data| {}, - .s_enum => |data| {}, - .s_namespace => |data| {}, - else => {}, + .stmt => |s2| { + switch (s2.data) { + .s_function => |func| { + var name: string = undefined; + if (func.func.name) |func_loc| { + name = p.symbols.items[func_loc.ref.?.inner_index].original_name; + } else { + func.func.name = data.default_name; + name = "default"; + } + + p.visitFunc(&func.func, func.func.open_parens_loc); + stmts.append(stmt.*) catch unreachable; + + if (func.func.name) |name_ref| { + // TODO-REACT-REFRESH-SPOT + stmts.append(p.keepStmtSymbolName(name_ref.loc, name_ref.ref.?, name)) catch unreachable; + } + }, + .s_class => |class| { + var shadow_ref = p.visitClass(s2.loc, &class.class); + }, + else => {}, + } + }, + } + }, + .s_export_equals => |data| { + // "module.exports = value" + stmts.append( + Expr.assignStmt( + p.e( + E.Dot{ + .target = p.e( + E.Identifier{ + .ref = p.module_ref, + }, + stmt.loc, + ), + .name = "exports", + .name_loc = stmt.loc, + }, + stmt.loc, + ), + p.visitExpr(data.value), + p.allocator, + ), + ) catch unreachable; + p.recordUsage(&p.module_ref); + }, + .s_break => |data| { + if (data.label) |*label| { + const name = p.loadNameFromRef(label.ref orelse p.panic("Expected label to have a ref", .{})); + const res = p.findLabelSymbol(label.loc, name); + + label.ref = res.ref; + } else if (p.fn_or_arrow_data_visit.is_inside_loop and !p.fn_or_arrow_data_visit.is_inside_switch) { + const r = js_lexer.rangeOfIdentifier(&p.source, stmt.loc); + p.log.addRangeError(p.source, r, "Cannot use \"break\" here") catch unreachable; + } + }, + .s_continue => |data| { + if (data.label) |*label| { + const name = p.loadNameFromRef(label.ref orelse p.panic("Expected continue label to have a ref", .{})); + const res = p.findLabelSymbol(label.loc, name); + label.ref = res.ref; + if (res.found and !res.is_loop) { + const r = js_lexer.rangeOfIdentifier(&p.source, stmt.loc); + p.log.addRangeErrorFmt(p.source, r, p.allocator, "Cannot \"continue\" to label {s}", .{name}) catch unreachable; + } + } else if (!p.fn_or_arrow_data_visit.is_inside_loop) { + const r = js_lexer.rangeOfIdentifier(&p.source, stmt.loc); + p.log.addRangeError(p.source, r, "Cannot use \"continue\" here") catch unreachable; + } + }, + .s_label => |data| { + p.pushScopeForVisitPass(.label, stmt.loc) catch unreachable; + const name = p.loadNameFromRef(data.name.ref orelse unreachable); + const ref = p.newSymbol(.label, name) catch unreachable; + data.name.ref = ref; + p.current_scope.label_ref = ref; + switch (data.stmt.data) { + .s_for, .s_for_in, .s_for_of, .s_while, .s_do_while => { + p.current_scope.label_stmt_is_loop = true; + }, + else => {}, + } + + data.stmt = p.visitSingleStmt(data.stmt, StmtsKind.none); + p.popScope(); + }, + .s_local => |data| { + for (data.decls) |*d| { + p.visitBinding(d.binding, null); + + if (d.value != null) { + var val = d.value orelse unreachable; + const was_anonymous_named_expr = p.isAnonymousNamedExpr(val); + + val = p.visitExpr(val); + // go version of defer would cause this to reset the variable + // zig version of defer causes this to set it to the last value of val, at the end of the scope. + defer d.value = val; + + // Optionally preserve the name + switch (d.binding.data) { + .b_identifier => |id| { + val = p.maybeKeepExprSymbolName( + val, + p.symbols.items[id.ref.inner_index].original_name, + was_anonymous_named_expr, + ); + }, + else => {}, + } + } + } + + // Handle being exported inside a namespace + if (data.is_export and p.enclosing_namespace_arg_ref != null) { + for (data.decls) |*d| { + if (d.value) |val| { + p.recordUsage(&(p.enclosing_namespace_arg_ref orelse unreachable)); + // TODO: is it necessary to lowerAssign? why does esbuild do it _most_ of the time? + stmts.append(p.s(S.SExpr{ + .value = Expr.assign(Binding.toExpr(&d.binding, p.to_expr_wrapper_namespace), val, p.allocator), + }, stmt.loc)) catch unreachable; + } + } + + return; + } + + // TODO: do we need to relocate vars? I don't think so. + if (data.kind == .k_var) {} + }, + .s_expr => |data| { + p.stmt_expr_value = data.value.data; + data.value = p.visitExpr(data.value); + + // TODO: + // if (p.options.mangle_syntax) { + + // } + }, + .s_throw => |data| { + data.value = p.visitExpr(data.value); + }, + .s_return => |data| { + if (p.fn_or_arrow_data_visit.is_outside_fn_or_arrow) { + const where = where: { + if (p.es6_export_keyword.len > 0) { + break :where p.es6_export_keyword; + } else if (p.top_level_await_keyword.len > 0) { + break :where p.top_level_await_keyword; + } else { + break :where logger.Range.None; + } + }; + + if (where.len > 0) { + p.log.addRangeError(p.source, where, "Top-level return cannot be used inside an ECMAScript module") catch unreachable; + } + } + + if (data.value) |val| { + data.value = p.visitExpr(val); + + // "return undefined;" can safely just always be "return;" + if (@as(Expr.Tag, data.value.?.data) == .e_undefined) { + // Returning undefined is implicit + data.value = null; + } + } + }, + .s_block => |data| { + { + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + defer p.popScope(); + + // Pass the "is loop body" status on to the direct children of a block used + // as a loop body. This is used to enable optimizations specific to the + // topmost scope in a loop body block. + const kind = if (std.meta.eql(p.loop_body, stmt.data)) StmtsKind.loop_body else StmtsKind.none; + var _stmts = List(Stmt).init(p.allocator); + p.visitStmts(&_stmts, kind) catch unreachable; + data.stmts = _stmts.toOwnedSlice(); + } + + // trim empty statements + if (data.stmts.len == 0) { + stmts.append(p.s(S.Empty{}, stmt.loc)) catch unreachable; + return; + } else if (data.stmts.len == 1 and !statementCaresAboutScope(data.stmts[0])) { + // Unwrap blocks containing a single statement + stmts.append(data.stmts[0]) catch unreachable; + return; + } + }, + .s_with => |data| { + notimpl(); + }, + .s_while => |data| { + data.test_ = p.visitExpr(data.test_); + data.body = p.visitLoopBody(data.body); + + // TODO: simplify boolean expression + }, + .s_do_while => |data| { + data.test_ = p.visitExpr(data.test_); + data.body = p.visitLoopBody(data.body); + + // TODO: simplify boolean expression + }, + .s_if => |data| { + data.test_ = p.visitExpr(data.test_); + + // TODO: Fold constants + + }, + .s_for => |data| { + { + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + defer p.popScope(); + if (data.init) |initst| { + _ = p.visitForLoopInit(initst, false); + } + + if (data.test_) |test_| { + data.test_ = p.visitExpr(test_); + + // TODO: boolean with side effects + } + + if (data.update) |update| { + data.update = p.visitExpr(update); + } + + data.body = p.visitLoopBody(data.body); + } + // TODO: Potentially relocate "var" declarations to the top level + + }, + .s_for_in => |data| { + { + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + defer p.popScope(); + _ = p.visitForLoopInit(data.init, true); + data.value = p.visitExpr(data.value); + data.body = p.visitLoopBody(data.body); + + // TODO: do we need to this? + // // Check for a variable initializer + // if local, ok := s.Init.Data.(*js_ast.SLocal); ok && local.Kind == js_ast.LocalVar && len(local.Decls) == 1 { + // decl := &local.Decls[0] + // if id, ok := decl.Binding.Data.(*js_ast.BIdentifier); ok && decl.Value != nil { + // p.markStrictModeFeature(forInVarInit, p.source.RangeOfOperatorBefore(decl.Value.Loc, "="), "") + + // // Lower for-in variable initializers in case the output is used in strict mode + // stmts = append(stmts, js_ast.Stmt{Loc: stmt.Loc, Data: &js_ast.SExpr{Value: js_ast.Assign( + // js_ast.Expr{Loc: decl.Binding.Loc, Data: &js_ast.EIdentifier{Ref: id.Ref}}, + // *decl.Value, + // )}}) + // decl.Value = nil + // } + // } + } + }, + .s_for_of => |data| { + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + defer p.popScope(); + _ = p.visitForLoopInit(data.init, true); + data.value = p.visitExpr(data.value); + data.body = p.visitLoopBody(data.body); + + // TODO: do we need to do this? + // // Potentially relocate "var" declarations to the top level + // if init, ok := s.Init.Data.(*js_ast.SLocal); ok && init.Kind == js_ast.LocalVar { + // if replacement, ok := p.maybeRelocateVarsToTopLevel(init.Decls, relocateVarsForInOrForOf); ok { + // s.Init = replacement + // } + // } + + // p.lowerObjectRestInForLoopInit(s.Init, &s.Body) + }, + .s_try => |data| { + notimpl(); + }, + .s_switch => |data| { + notimpl(); + }, + .s_function => |data| { + notimpl(); + }, + .s_class => |data| { + notimpl(); + }, + .s_enum => |data| { + notimpl(); + }, + .s_namespace => |data| { + notimpl(); + }, + else => { + notimpl(); + }, } // if we get this far, it stays try stmts.append(stmt.*); } + pub fn visitForLoopInit(p: *P, stmt: Stmt, is_in_or_of: bool) Stmt { + switch (stmt.data) { + .s_expr => |st| { + const assign_target = if (is_in_or_of) js_ast.AssignTarget.replace else js_ast.AssignTarget.none; + p.stmt_expr_value = st.value.data; + st.value = p.visitExprInOut(st.value, ExprIn{ .assign_target = assign_target }); + }, + .s_local => |st| { + for (st.decls) |*dec| { + p.visitBinding(dec.binding, null); + if (dec.value) |val| { + dec.value = p.visitExpr(val); + } + } + // s.Decls = p.lowerObjectRestInDecls(s.Decls) + // s.Kind = p.selectLocalKind(s.Kind) + }, + else => { + p.panic("Unexpected stmt in visitForLoopInit: {s}", .{stmt}); + }, + } + + return stmt; + } + + // pub fn maybeRelocateVarsToTopLevel(p: *P, decls: []G.Decl, mode: ) + + pub fn wrapIdentifierNamespace( + p: *P, + loc: logger.Loc, + ref: Ref, + ) Expr { + p.recordUsage(&(p.enclosing_namespace_arg_ref orelse unreachable)); + + return p.e(E.Dot{ + .target = p.e(E.Identifier{ .ref = p.enclosing_namespace_arg_ref orelse unreachable }, loc), + .name = p.symbols.items[ref.inner_index].original_name, + .name_loc = loc, + }, loc); + } + + pub fn wrapIdentifierHoisting( + p: *P, + loc: logger.Loc, + ref: Ref, + ) Expr { + p.relocated_top_level_vars.append(LocRef{ .loc = loc, .ref = ref }) catch unreachable; + var _ref = ref; + p.recordUsage(&_ref); + return p.e(E.Identifier{ .ref = _ref }, loc); + } + + pub fn isAnonymousNamedExpr(p: *P, expr: ExprNodeIndex) bool { + notimpl(); + } + + pub fn visitBinding(p: *P, binding: BindingNodeIndex, duplicate_arg_check: ?StringBoolMap) void { + notimpl(); + } + + pub fn visitLoopBody(p: *P, stmt: StmtNodeIndex) StmtNodeIndex { + const old_is_inside_loop = p.fn_or_arrow_data_visit.is_inside_loop; + p.fn_or_arrow_data_visit.is_inside_loop = true; + defer p.fn_or_arrow_data_visit.is_inside_loop = old_is_inside_loop; + p.loop_body = stmt.data; + return p.visitSingleStmt(stmt, .loop_body); + } + + pub fn visitSingleStmt(p: *P, stmt: Stmt, kind: StmtsKind) Stmt { + const has_if_scope = has_if: { + switch (stmt.data) { + .s_function => |func| { + break :has_if func.func.flags.has_if_scope; + }, + else => { + break :has_if false; + }, + } + }; + + // Introduce a fake block scope for function declarations inside if statements + if (has_if_scope) { + p.pushScopeForVisitPass(.block, stmt.loc) catch unreachable; + } + + var stmts = List(Stmt).initCapacity(p.allocator, 1) catch unreachable; + stmts.append(stmt) catch unreachable; + p.visitStmts(&stmts, kind) catch unreachable; + + if (has_if_scope) { + p.popScope(); + } + + return p.stmtsToSingleStmt(stmt.loc, stmts.toOwnedSlice()); + } + + // One statement could potentially expand to several statements + pub fn stmtsToSingleStmt(p: *P, loc: logger.Loc, stmts: []Stmt) Stmt { + if (stmts.len == 0) { + return p.s(S.Empty{}, loc); + } + + if (stmts.len == 1) { + switch (stmts[0].data) { + .s_local => |local| { + // "let" and "const" must be put in a block when in a single-statement context + + if (local.kind == .k_var) { + return stmts[0]; + } + }, + else => { + return stmts[0]; + }, + } + } + + return p.s(S.Block{ .stmts = stmts }, loc); + } + + pub fn findLabelSymbol(p: *P, loc: logger.Loc, name: string) FindLabelSymbolResult { + var res = FindLabelSymbolResult{ .ref = undefined, .is_loop = false }; + + var _scope: ?*Scope = p.current_scope; + + while (_scope) |scope| : (_scope = scope.parent) { + var label_ref = scope.label_ref orelse continue; + + if (!scope.kindStopsHoisting() or (scope.kind != .label) or !strings.eql(name, p.symbols.items[label_ref.inner_index].original_name)) { + continue; + } + + // Track how many times we've referenced this symbol + p.recordUsage(&label_ref); + res.ref = label_ref; + res.is_loop = scope.label_stmt_is_loop; + res.found = true; + break; + } + + const r = js_lexer.rangeOfIdentifier(&p.source, loc); + p.log.addRangeErrorFmt(p.source, r, p.allocator, "There is no containing label named {s}", .{name}) catch unreachable; + + // Allocate an "unbound" symbol + var ref = p.newSymbol(.unbound, name) catch unreachable; + + // Track how many times we've referenced this symbol + p.recordUsage(&ref); + + return res; + } + + pub fn visitClass(p: *P, loc: logger.Loc, class: *G.Class) Ref { + notimpl(); + } + + fn keepStmtSymbolName(p: *P, loc: logger.Loc, ref: Ref, name: string) Stmt { + var exprs = p.allocator.alloc(Expr, 2) catch unreachable; + exprs[0] = p.e(E.Identifier{ + .ref = ref, + }, loc); + exprs[1] = p.e(E.String{ .value = strings.toUTF16Alloc(name, p.allocator) catch unreachable }, loc); + return p.s(S.SExpr{ + // I believe that this is a spot we can do $RefreshReg$(name) + .value = p.callRuntime(loc, "__name", exprs), + + // Make sure tree shaking removes this if the function is never used + .does_not_affect_tree_shaking = true, + }, loc); + } + + pub fn callRuntime(p: *P, loc: logger.Loc, name: string, args: []Expr) Expr { + var ref: Ref = undefined; + if (!p.runtime_imports.contains(name)) { + ref = p.newSymbol(.other, name) catch unreachable; + p.module_scope.generated.append(ref) catch unreachable; + p.runtime_imports.put(name, ref) catch unreachable; + } else { + ref = p.runtime_imports.get(name) orelse unreachable; + } + + p.recordUsage(&ref); + return p.e(E.Call{ + .target = p.e(E.Identifier{ + .ref = ref, + }, loc), + .args = args, + }, loc); + } + fn visitStmts(p: *P, stmts: *List(Stmt), kind: StmtsKind) !void { // Save the current control-flow liveness. This represents if we are // currently inside an "if (false) { ... }" block. @@ -6178,8 +6909,7 @@ const P = struct { if (p.options.ts and p.lexer.token == .t_equals and !p.forbid_suffix_after_as_loc.eql(p.lexer.loc())) { p.lexer.next(); - var expr = p.parseExpr(.comma); - item = item.assign(&expr, p.allocator); + item = Expr.assign(item, p.parseExpr(.comma), p.allocator); } items_list.append(item) catch unreachable; @@ -6274,10 +7004,15 @@ const P = struct { parser.log = log; parser.allocator = allocator; parser.options = opts; + parser.to_expr_wrapper_namespace = Binding2ExprWrapper.Namespace.init(parser); + parser.to_expr_wrapper_hoisted = Binding2ExprWrapper.Hoisted.init(parser); parser.source = source; + parser.lexer = lexer; parser.data = js_ast.AstData.init(allocator); + _ = try parser.pushScopeForParsePass(.entry, locModuleScope); + return parser; } }; @@ -6309,11 +7044,48 @@ const DeferredArrowArgErrors = struct { invalid_expr_yield: logger.Range = logger.Range.None, }; -test "js_parser.init" { - try alloc.setup(std.heap.page_allocator); +const SymbolList = [][]Symbol; + +fn expectPrintedJS(contents: string, expected: string) !void { + if (alloc.dynamic_manager == null) { + try alloc.setup(std.heap.page_allocator); + } + + debugl("INIT TEST"); + + const opts = try options.TransformOptions.initUncached(alloc.dynamic, "file.js", contents); + var log = logger.Log.init(alloc.dynamic); + var source = logger.Source.initFile(opts.entry_point, alloc.dynamic); + var ast: js_ast.Ast = undefined; + + debugl("INIT PARSER"); + + var parser = try Parser.init(opts, &log, &source, alloc.dynamic); + debugl("RUN PARSER"); + + var res = try parser.parse(); + ast = res.ast; + var symbols: SymbolList = &([_][]Symbol{ast.symbols}); + var symbol_map = js_ast.Symbol.Map.initList(symbols); + + if (log.msgs.items.len > 0) { + debugl("PRINT LOG ERRORS"); + var fixedBuffer = [_]u8{0} ** 4096; + var stream = std.io.fixedBufferStream(&fixedBuffer); + + try log.print(stream.writer()); + std.debug.print("{s}", .{fixedBuffer}); + } + var linker = @import("linker.zig").Linker{}; + debugl("START AST PRINT"); + const result = js_printer.printAst(alloc.dynamic, ast, symbol_map, true, js_printer.Options{ .to_module_ref = res.ast.module_ref orelse Ref{ .inner_index = 0 } }, &linker) catch unreachable; + + std.testing.expectEqualStrings(contents, result.js); +} - const entryPointName = "/bacon/hello.js"; - const code = "for (let i = 0; i < 100; i++) { console.log(\"hi\");\n}"; - var parser = try Parser.init(try options.TransformOptions.initUncached(alloc.dynamic, entryPointName, code), alloc.dynamic); - const res = try parser.parse(); +test "expectPrint" { + try expectPrintedJS( + "const bacon = true; function hello() { return 100; }; hello();", + "const bacon = true; function hello() { return 100; }; hello();", + ); } |