diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 753e639934..8eb3e39290 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -46,6 +46,8 @@ jobs: run: | bun scripts/zig-remove-unreferenced-top-level-decls.ts src/ zig fmt src + bun scripts/sortImports src + zig fmt src - name: Commit uses: stefanzweifel/git-auto-commit-action@v5 with: diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 59841022f3..4ea65974b3 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -10,7 +10,26 @@ src/allocators/NullableAllocator.zig src/analytics/analytics_schema.zig src/analytics/analytics_thread.zig src/api/schema.zig +src/ast/Ast.zig +src/ast/ASTMemoryAllocator.zig +src/ast/B.zig src/ast/base.zig +src/ast/Binding.zig +src/ast/BundledAst.zig +src/ast/CharFreq.zig +src/ast/E.zig +src/ast/Expr.zig +src/ast/G.zig +src/ast/Macro.zig +src/ast/NewStore.zig +src/ast/Op.zig +src/ast/S.zig +src/ast/Scope.zig +src/ast/ServerComponentBoundary.zig +src/ast/Stmt.zig +src/ast/Symbol.zig +src/ast/TS.zig +src/ast/UseDirective.zig src/async/posix_event_loop.zig src/async/stub_event_loop.zig src/async/windows_event_loop.zig diff --git a/scripts/longest.js b/scripts/longest.js new file mode 100644 index 0000000000..df78b0e85e --- /dev/null +++ b/scripts/longest.js @@ -0,0 +1,125 @@ +const fs = require("fs"); +const path = require("path"); + +// Regex patterns for different types of top-level declarations +const DECLARATION_PATTERN = + // pub? (export|extern)? (const|fn|var) name + /^(pub\s+)?(export\s+|extern\s+)?(const|fn|var)\s+([a-zA-Z_][a-zA-Z0-9_]*)/; + +function findDeclarations(filePath) { + const content = fs.readFileSync(filePath, "utf8"); + const lines = content.split("\n"); + const declarations = []; + + // First pass: collect all declarations with their line numbers + for (let lineNum = 0; lineNum < lines.length; lineNum++) { + const line = lines[lineNum]; + + // Skip empty lines and comments + if (!line || line.trim().startsWith("//") || line.trim().startsWith("///")) { + continue; + } + + // Only process top-level declarations (no indentation) + if (line.startsWith(" ") || line.startsWith("\t")) { + continue; + } + + const trimmedLine = line.trim(); + + // Check each pattern + const match = trimmedLine.match(DECLARATION_PATTERN); + if (match) { + // Extract the name from the match + const name = match[match.length - 1]; // Last capture group is the name + + declarations.push({ + name, + match: match[0], + line: lineNum + 1, + type: getDeclarationType(match[0]), + fullLine: trimmedLine, + startLine: lineNum, + }); + } + } + + // Second pass: calculate sizes based on next declaration's start line + for (let i = 0; i < declarations.length; i++) { + const currentDecl = declarations[i]; + const nextDecl = declarations[i + 1]; + + if (nextDecl) { + // Size is from current declaration start to next declaration start + currentDecl.size = nextDecl.startLine - currentDecl.startLine; + } else { + // Last declaration: size is from current declaration start to end of file + currentDecl.size = lines.length - currentDecl.startLine; + } + } + + return declarations; +} + +function getDeclarationType(matchText) { + if (matchText.includes("const")) return "const"; + if (matchText.includes("fn")) return "fn"; + if (matchText.includes("var")) return "var"; + return "unknown"; +} + +function main() { + const args = process.argv.slice(2); + + if (args.length === 0) { + console.error("Usage: bun longest.js "); + console.error("Example: bun longest.js src/walker_skippable.zig"); + process.exit(1); + } + + const filePath = args[0]; + + if (!fs.existsSync(filePath)) { + console.error(`File not found: ${filePath}`); + process.exit(1); + } + + if (!filePath.endsWith(".zig")) { + console.error("Please provide a .zig file"); + process.exit(1); + } + + try { + const declarations = findDeclarations(filePath); + + if (declarations.length === 0) { + console.log("No top-level declarations found."); + return; + } + + console.log(`Found ${declarations.length} top-level declarations in ${filePath}:\n`); + + // Sort by declaration size (smallest first) + declarations.sort((a, b) => a.size - b.size); + + // Find the longest name for formatting + const maxNameLength = Math.max(...declarations.map(d => d.match.length)); + const maxTypeLength = Math.max(...declarations.map(d => d.type.length)); + + console.log(`${"Name".padEnd(maxNameLength + 2)} ${"Type".padEnd(maxTypeLength + 2)} ${"Num Lines".padEnd(6)}`); + console.log("-".repeat(maxNameLength + maxTypeLength + 15)); + + declarations.forEach(decl => { + console.log( + `${decl.match.padEnd(maxNameLength + 2)} ${decl.type.padEnd(maxTypeLength + 2)} ${decl.size.toString().padEnd(6)}`, + ); + }); + } catch (error) { + console.error("Error reading file:", error.message); + process.exit(1); + } +} + +if (require.main === module) { + main(); +} diff --git a/scripts/sortImports.ts b/scripts/sortImports.ts new file mode 100644 index 0000000000..4113aff3a1 --- /dev/null +++ b/scripts/sortImports.ts @@ -0,0 +1,394 @@ +import { readdirSync } from "fs"; +import path from "path"; + +// Parse command line arguments +const args = process.argv.slice(2); + +const filePaths = args.filter(arg => !arg.startsWith("-")); +const usage = String.raw` + __ .__ __ + ____________________/ |_ _______|__| _____ ______ ____________/ |_ ______ + \___ / _ \_ __ \ __\ \___ / |/ \\____ \ / _ \_ __ \ __\/ ___/ + / ( <_> ) | \/| | / /| | Y Y \ |_> > <_> ) | \/| | \___ \ + /_____ \____/|__| |__| /_____ \__|__|_| / __/ \____/|__| |__| /____ > + \/ \/ \/|__| \/ + +Usage: bun scripts/sortImports [options] + +Options: + --help Show this help message + --no-include-pub Exclude pub imports from sorting + --no-remove-unused Don't remove unused imports + --include-unsorted Process files even if they don't have @sortImports marker + +Examples: + bun scripts/sortImports src +`.slice(1); +if (args.includes("--help")) { + console.log(usage); + process.exit(0); +} +if (filePaths.length === 0) { + console.error(usage); + process.exit(1); +} + +const config = { + includePub: !args.includes("--no-include-pub"), + removeUnused: !args.includes("--no-remove-unused"), + includeUnsorted: args.includes("--include-unsorted"), +}; + +// Type definitions +type Declaration = { + index: number; + key: string; + value: string; + segments: string[] | null; + whole: string; + last?: string; + wholepath?: string[]; +}; + +// Parse declarations from the file +function parseDeclarations( + lines: string[], + fileContents: string, +): { + declarations: Map; + unusedLineIndices: number[]; +} { + const declarations = new Map(); + const unusedLineIndices: number[] = []; + + // for stability + const sortedLineKeys = [...lines.keys()].sort((a, b) => (lines[a] < lines[b] ? -1 : lines[a] > lines[b] ? 1 : 0)); + + for (const i of sortedLineKeys) { + const line = lines[i]; + + if (line === "// @sortImports") { + lines[i] = ""; + continue; + } + + const inlineDeclPattern = /^(?:pub )?const ([a-zA-Z0-9_]+) = (.+);$/; + const match = line.match(inlineDeclPattern); + + if (!match) continue; + + const name = match[1]; + const value = match[2]; + + // Skip if the previous line has a doc comment + const prevLine = lines[i - 1] ?? ""; + if (prevLine.startsWith("///")) { + continue; + } + + // Skip unused declarations (non-public declarations that appear only once) + if (config.removeUnused && !line.includes("pub ")) { + const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const expectedCount = (line.match(new RegExp(`\\b${escapedName}\\b`, "g")) || []).length; + const actualCount = (fileContents.match(new RegExp(`\\b${escapedName}\\b`, "g")) || []).length; + if (expectedCount === actualCount) { + // unused decl + unusedLineIndices.push(i); + continue; + } + } + + if (!config.includePub && line.includes("pub ")) { + continue; + } + + declarations.set(name, { + whole: line, + index: i, + key: name, + value, + segments: parseSegments(value), + }); + } + + return { declarations, unusedLineIndices }; +} + +// Validate if a segment is a valid identifier +function isValidSegment(segment: string): boolean { + if (segment.startsWith("@import(") || segment === "@This()") { + return true; + } + return segment.match(/^[a-zA-Z0-9_]+$/) != null; +} + +// Parse import path segments from a value +function parseSegments(value: string): null | string[] { + if (value.startsWith("@import(")) { + const rightBracketIndex = value.indexOf(")"); + if (rightBracketIndex === -1) return null; + + const importPart = value.slice(0, rightBracketIndex + 1); + const remainingPart = value.slice(rightBracketIndex + 1); + + if (remainingPart.startsWith(".")) { + const segments = remainingPart.slice(1).split("."); + if (!segments.every(segment => isValidSegment(segment))) return null; + return [importPart, ...segments]; + } else if (remainingPart === "") { + return [importPart]; + } else { + return null; + } + } else { + const segments = value.split("."); + if (!segments.every(segment => isValidSegment(segment))) return null; + return segments; + } +} + +// Resolve the first segment of an import path +function resolveFirstSegment(firstSegment: string, declarations: Map): null | string[] { + if (firstSegment.startsWith("@import(") || firstSegment.startsWith("@This()")) { + return [firstSegment]; + } else { + const declaration = declarations.get(firstSegment); + if (!declaration) { + return null; // Unknown declaration + } + + const subFirstSegment = declaration.segments?.[0]; + if (!subFirstSegment) { + return null; // Invalid declaration + } + + const resolvedSubFirst = resolveFirstSegment(subFirstSegment, declarations); + if (!resolvedSubFirst) { + return null; // Unable to resolve + } + + return [...resolvedSubFirst, ...(declaration.segments?.slice(1) ?? [])]; + } +} + +type Group = { + keySegments: string[]; + declarations: Declaration[]; +}; + +// Group declarations by their import paths +function groupDeclarationsByImportPath(declarations: Map): Map { + const groups = new Map(); + + for (const declaration of declarations.values()) { + if (!declaration.segments || declaration.segments.length < 1) { + continue; + } + + const firstSegment = declaration.segments[0]; + const resolvedFirst = resolveFirstSegment(firstSegment, declarations); + + if (!resolvedFirst) { + continue; + } + + const remainingSegments = declaration.segments.slice(1); + const fullPath = [...resolvedFirst, ...remainingSegments]; + const lastSegment = fullPath.pop(); + + if (!lastSegment) { + continue; + } + + const groupKey = fullPath.join("."); + if (!groups.has(groupKey)) { + groups.set(groupKey, { keySegments: fullPath, declarations: [] }); + } + + groups.get(groupKey)!.declarations.push(declaration); + declaration.last = lastSegment; + declaration.wholepath = [...fullPath, lastSegment]; + } + + return groups; +} + +// Merge single-item groups into their parent groups +function mergeSingleItemGroups(groups: Map): void { + while (true) { + let hasChanges = false; + + for (const [groupKey, group] of groups.entries()) { + if (group.declarations.length === 1) { + const gcsplit = [...group.keySegments]; + while (gcsplit.pop()) { + const parentKey = gcsplit.join("."); + if (groups.has(parentKey)) { + groups.get(parentKey)!.declarations.push(group.declarations[0]); + groups.delete(groupKey); + hasChanges = true; + break; + } + } + } + } + + if (!hasChanges) break; + } +} + +// Move items with child groups to the top of those child groups +function promoteItemsWithChildGroups(groups: Map): void { + for (const [groupKey, group] of groups.entries()) { + for (let i = 0; i < group.declarations.length; ) { + const item = group.declarations[i]; + const childGroupKey = (groupKey ? groupKey + "." : "") + item.last; + + if (groups.has(childGroupKey)) { + groups.get(childGroupKey)!.declarations.unshift(item); + group.declarations.splice(i, 1); + } else { + i++; + } + } + } +} + +// Sort groups and their declarations +function sortGroupsAndDeclarations(groups: Map): string[] { + // Sort declarations within each group + for (const group of groups.values()) { + group.declarations.sort((a, b) => { + if (a.wholepath?.length !== b.wholepath?.length) { + return (a.wholepath?.length ?? 0) - (b.wholepath?.length ?? 0); + } + return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; + }); + } + + // Sort group keys alphabetically + return Array.from(groups.keys()).sort((a, b) => { + return a < b ? -1 : a > b ? 1 : 0; + }); +} + +// Generate the sorted output +function generateSortedOutput(lines: string[], groups: Map, sortedGroupKeys: string[]): string[] { + const outputLines = [...lines]; + outputLines.push(""); + outputLines.push("// @sortImports"); + + for (const groupKey of sortedGroupKeys) { + const groupDeclarations = groups.get(groupKey)!; + if (!groupDeclarations?.declarations.length) continue; + + // Add spacing between groups + outputLines.push(""); + + // Add declarations to output and mark original lines for removal + for (const declaration of groupDeclarations.declarations) { + outputLines.push(declaration.whole); + outputLines[declaration.index] = ""; + } + } + + return outputLines; +} + +// Main execution function for a single file +async function processFile(filePath: string): Promise { + const originalFileContents = await Bun.file(filePath).text(); + let fileContents = originalFileContents; + + if (!config.includeUnsorted && !originalFileContents.includes("// @sortImports")) { + return; + } + console.log(`Processing: ${filePath}`); + + let needsRecurse = true; + while (needsRecurse) { + needsRecurse = false; + + const lines = fileContents.split("\n"); + + const { declarations, unusedLineIndices } = parseDeclarations(lines, fileContents); + const groups = groupDeclarationsByImportPath(declarations); + + promoteItemsWithChildGroups(groups); + mergeSingleItemGroups(groups); + const sortedGroupKeys = sortGroupsAndDeclarations(groups); + + const sortedLines = generateSortedOutput(lines, groups, sortedGroupKeys); + + // Remove unused declarations + if (config.removeUnused) { + for (const line of unusedLineIndices) { + sortedLines[line] = ""; + needsRecurse = true; + } + } + fileContents = sortedLines.join("\n"); + } + + // Remove any leading newlines + fileContents = fileContents.replace(/^\n+/, ""); + + // Maximum of one empty line + fileContents = fileContents.replace(/\n\n+/g, "\n\n"); + + // Ensure exactly one trailing newline + fileContents = fileContents.replace(/\s*$/, "\n"); + + // If the file is empty, remove the trailing newline + if (fileContents === "\n") fileContents = ""; + + if (fileContents === originalFileContents) { + console.log(`✓ No changes: ${filePath}`); + return; + } + + // Write the sorted file + await Bun.write(filePath, fileContents); + + console.log(`✓ Done: ${filePath}`); +} + +// Process all files +async function main() { + let successCount = 0; + let errorCount = 0; + + for (const filePath of filePaths) { + const stat = await Bun.file(filePath).stat(); + if (stat.isDirectory()) { + const files = readdirSync(filePath, { recursive: true }); + for (const file of files) { + if (typeof file !== "string" || !file.endsWith(".zig")) continue; + try { + await processFile(path.join(filePath, file)); + successCount++; + } catch (error) { + errorCount++; + console.error(`Failed to process ${filePath}`); + } + } + continue; + } + + try { + await processFile(filePath); + successCount++; + } catch (error) { + errorCount++; + console.error(`Failed to process ${filePath}`); + } + } + + console.log(`\nSummary: ${successCount} files processed successfully, ${errorCount} errors`); + + if (errorCount > 0) { + process.exit(1); + } +} + +main(); diff --git a/src/ast/ASTMemoryAllocator.zig b/src/ast/ASTMemoryAllocator.zig new file mode 100644 index 0000000000..bfecc27688 --- /dev/null +++ b/src/ast/ASTMemoryAllocator.zig @@ -0,0 +1,96 @@ +const SFA = std.heap.StackFallbackAllocator(@min(8192, std.heap.page_size_min)); + +stack_allocator: SFA = undefined, +bump_allocator: std.mem.Allocator = undefined, +allocator: std.mem.Allocator, +previous: ?*ASTMemoryAllocator = null, + +pub fn enter(this: *ASTMemoryAllocator, allocator: std.mem.Allocator) ASTMemoryAllocator.Scope { + this.allocator = allocator; + this.stack_allocator = SFA{ + .buffer = undefined, + .fallback_allocator = allocator, + .fixed_buffer_allocator = undefined, + }; + this.bump_allocator = this.stack_allocator.get(); + this.previous = null; + var ast_scope = ASTMemoryAllocator.Scope{ + .current = this, + .previous = Stmt.Data.Store.memory_allocator, + }; + ast_scope.enter(); + return ast_scope; +} +pub const Scope = struct { + current: ?*ASTMemoryAllocator = null, + previous: ?*ASTMemoryAllocator = null, + + pub fn enter(this: *@This()) void { + bun.debugAssert(Expr.Data.Store.memory_allocator == Stmt.Data.Store.memory_allocator); + + this.previous = Expr.Data.Store.memory_allocator; + + const current = this.current; + + Expr.Data.Store.memory_allocator = current; + Stmt.Data.Store.memory_allocator = current; + + if (current == null) { + Stmt.Data.Store.begin(); + Expr.Data.Store.begin(); + } + } + + pub fn exit(this: *const @This()) void { + Expr.Data.Store.memory_allocator = this.previous; + Stmt.Data.Store.memory_allocator = this.previous; + } +}; + +pub fn reset(this: *ASTMemoryAllocator) void { + this.stack_allocator = SFA{ + .buffer = undefined, + .fallback_allocator = this.allocator, + .fixed_buffer_allocator = undefined, + }; + this.bump_allocator = this.stack_allocator.get(); +} + +pub fn push(this: *ASTMemoryAllocator) void { + Stmt.Data.Store.memory_allocator = this; + Expr.Data.Store.memory_allocator = this; +} + +pub fn pop(this: *ASTMemoryAllocator) void { + const prev = this.previous; + bun.assert(prev != this); + Stmt.Data.Store.memory_allocator = prev; + Expr.Data.Store.memory_allocator = prev; + this.previous = null; +} + +pub fn append(this: ASTMemoryAllocator, comptime ValueType: type, value: anytype) *ValueType { + const ptr = this.bump_allocator.create(ValueType) catch unreachable; + ptr.* = value; + return ptr; +} + +/// Initialize ASTMemoryAllocator as `undefined`, and call this. +pub fn initWithoutStack(this: *ASTMemoryAllocator, arena: std.mem.Allocator) void { + this.stack_allocator = SFA{ + .buffer = undefined, + .fallback_allocator = arena, + .fixed_buffer_allocator = .init(&.{}), + }; + this.bump_allocator = this.stack_allocator.get(); +} + +// @sortImports + +const bun = @import("bun"); +const std = @import("std"); + +const js_ast = bun.js_ast; +const ASTMemoryAllocator = js_ast.ASTMemoryAllocator; +const Expr = js_ast.Expr; +const Stmt = js_ast.Stmt; diff --git a/src/ast/Ast.zig b/src/ast/Ast.zig new file mode 100644 index 0000000000..e51f1a2abd --- /dev/null +++ b/src/ast/Ast.zig @@ -0,0 +1,143 @@ +pub const TopLevelSymbolToParts = std.ArrayHashMapUnmanaged(Ref, BabyList(u32), Ref.ArrayHashCtx, false); + +approximate_newline_count: usize = 0, +has_lazy_export: bool = false, +runtime_imports: Runtime.Imports = .{}, + +nested_scope_slot_counts: SlotCounts = SlotCounts{}, + +runtime_import_record_id: ?u32 = null, +needs_runtime: bool = false, +// This is a list of CommonJS features. When a file uses CommonJS features, +// it's not a candidate for "flat bundling" and must be wrapped in its own +// closure. +has_top_level_return: bool = false, +uses_exports_ref: bool = false, +uses_module_ref: bool = false, +uses_require_ref: bool = false, +commonjs_module_exports_assigned_deoptimized: bool = false, + +force_cjs_to_esm: bool = false, +exports_kind: ExportsKind = ExportsKind.none, + +// This is a list of ES6 features. They are ranges instead of booleans so +// that they can be used in log messages. Check to see if "Len > 0". +import_keyword: logger.Range = logger.Range.None, // Does not include TypeScript-specific syntax or "import()" +export_keyword: logger.Range = logger.Range.None, // Does not include TypeScript-specific syntax +top_level_await_keyword: logger.Range = logger.Range.None, + +/// These are stored at the AST level instead of on individual AST nodes so +/// they can be manipulated efficiently without a full AST traversal +import_records: ImportRecord.List = .{}, + +hashbang: string = "", +directive: ?string = null, +parts: Part.List = Part.List{}, +// This list may be mutated later, so we should store the capacity +symbols: Symbol.List = Symbol.List{}, +module_scope: Scope = Scope{}, +char_freq: ?CharFreq = null, +exports_ref: Ref = Ref.None, +module_ref: Ref = Ref.None, +/// When using format .bake_internal_dev, this is the HMR variable instead +/// of the wrapper. This is because that format does not store module +/// wrappers in a variable. +wrapper_ref: Ref = Ref.None, +require_ref: Ref = Ref.None, + +// These are used when bundling. They are filled in during the parser pass +// since we already have to traverse the AST then anyway and the parser pass +// is conveniently fully parallelized. +named_imports: NamedImports = .{}, +named_exports: NamedExports = .{}, +export_star_import_records: []u32 = &([_]u32{}), + +// allocator: std.mem.Allocator, +top_level_symbols_to_parts: TopLevelSymbolToParts = .{}, + +commonjs_named_exports: CommonJSNamedExports = .{}, + +redirect_import_record_index: ?u32 = null, + +/// Only populated when bundling +target: bun.options.Target = .browser, +// const_values: ConstValuesMap = .{}, +ts_enums: TsEnumsMap = .{}, + +/// Not to be confused with `commonjs_named_exports` +/// This is a list of named exports that may exist in a CommonJS module +/// We use this with `commonjs_at_runtime` to re-export CommonJS +has_commonjs_export_names: bool = false, +import_meta_ref: Ref = Ref.None, + +pub const CommonJSNamedExport = struct { + loc_ref: LocRef, + needs_decl: bool = true, +}; +pub const CommonJSNamedExports = bun.StringArrayHashMapUnmanaged(CommonJSNamedExport); + +pub const NamedImports = std.ArrayHashMapUnmanaged(Ref, NamedImport, RefHashCtx, true); +pub const NamedExports = bun.StringArrayHashMapUnmanaged(NamedExport); +pub const ConstValuesMap = std.ArrayHashMapUnmanaged(Ref, Expr, RefHashCtx, false); +pub const TsEnumsMap = std.ArrayHashMapUnmanaged(Ref, bun.StringHashMapUnmanaged(InlinedEnumValue), RefHashCtx, false); + +pub fn fromParts(parts: []Part) Ast { + return Ast{ + .parts = Part.List.init(parts), + .runtime_imports = .{}, + }; +} + +pub fn initTest(parts: []Part) Ast { + return Ast{ + .parts = Part.List.init(parts), + .runtime_imports = .{}, + }; +} + +pub const empty = Ast{ .parts = Part.List{}, .runtime_imports = .{} }; + +pub fn toJSON(self: *const Ast, _: std.mem.Allocator, stream: anytype) !void { + const opts = std.json.StringifyOptions{ .whitespace = std.json.StringifyOptions.Whitespace{ + .separator = true, + } }; + try std.json.stringify(self.parts, opts, stream); +} + +/// Do not call this if it wasn't globally allocated! +pub fn deinit(this: *Ast) void { + // TODO: assert mimalloc-owned memory + if (this.parts.len > 0) this.parts.deinitWithAllocator(bun.default_allocator); + if (this.symbols.len > 0) this.symbols.deinitWithAllocator(bun.default_allocator); + if (this.import_records.len > 0) this.import_records.deinitWithAllocator(bun.default_allocator); +} + +// @sortImports + +const std = @import("std"); +const Runtime = @import("../runtime.zig").Runtime; + +const bun = @import("bun"); +const BabyList = bun.BabyList; +const ImportRecord = bun.ImportRecord; +const logger = bun.logger; +const string = bun.string; + +const js_ast = bun.js_ast; +const Ast = js_ast.Ast; +const CharFreq = js_ast.CharFreq; +const ExportsKind = js_ast.ExportsKind; +const Expr = js_ast.Expr; +const InlinedEnumValue = js_ast.InlinedEnumValue; +const LocRef = js_ast.LocRef; +const NamedExport = js_ast.NamedExport; +const NamedImport = js_ast.NamedImport; +const Part = js_ast.Part; +const Ref = js_ast.Ref; +const RefHashCtx = js_ast.RefHashCtx; +const Scope = js_ast.Scope; +const SlotCounts = js_ast.SlotCounts; +const Symbol = js_ast.Symbol; + +const G = js_ast.G; +pub const Class = G.Class; diff --git a/src/ast/B.zig b/src/ast/B.zig new file mode 100644 index 0000000000..4c66f7c4dc --- /dev/null +++ b/src/ast/B.zig @@ -0,0 +1,106 @@ +/// B is for Binding! Bindings are on the left side of variable +/// declarations (s_local), which is how destructuring assignments +/// are represented in memory. Consider a basic example. +/// +/// let hello = world; +/// ^ ^ +/// | E.Identifier +/// B.Identifier +/// +/// Bindings can be nested +/// +/// B.Array +/// | B.Identifier +/// | | +/// let { foo: [ bar ] } = ... +/// ---------------- +/// B.Object +pub const B = union(Binding.Tag) { + // let x = ... + b_identifier: *B.Identifier, + // let [a, b] = ... + b_array: *B.Array, + // let { a, b: c } = ... + b_object: *B.Object, + // this is used to represent array holes + b_missing: B.Missing, + + pub const Identifier = struct { + ref: Ref, + }; + + pub const Property = struct { + flags: Flags.Property.Set = Flags.Property.None, + key: ExprNodeIndex, + value: Binding, + default_value: ?Expr = null, + }; + + pub const Object = struct { + properties: []B.Property, + is_single_line: bool = false, + + pub const Property = B.Property; + }; + + pub const Array = struct { + items: []ArrayBinding, + has_spread: bool = false, + is_single_line: bool = false, + + pub const Item = ArrayBinding; + }; + + pub const Missing = struct {}; + + /// This hash function is currently only used for React Fast Refresh transform. + /// This doesn't include the `is_single_line` properties, as they only affect whitespace. + pub fn writeToHasher(b: B, hasher: anytype, symbol_table: anytype) void { + switch (b) { + .b_identifier => |id| { + const original_name = id.ref.getSymbol(symbol_table).original_name; + writeAnyToHasher(hasher, .{ std.meta.activeTag(b), original_name.len }); + }, + .b_array => |array| { + writeAnyToHasher(hasher, .{ std.meta.activeTag(b), array.has_spread, array.items.len }); + for (array.items) |item| { + writeAnyToHasher(hasher, .{item.default_value != null}); + if (item.default_value) |default| { + default.data.writeToHasher(hasher, symbol_table); + } + item.binding.data.writeToHasher(hasher, symbol_table); + } + }, + .b_object => |object| { + writeAnyToHasher(hasher, .{ std.meta.activeTag(b), object.properties.len }); + for (object.properties) |property| { + writeAnyToHasher(hasher, .{ property.default_value != null, property.flags }); + if (property.default_value) |default| { + default.data.writeToHasher(hasher, symbol_table); + } + property.key.data.writeToHasher(hasher, symbol_table); + property.value.data.writeToHasher(hasher, symbol_table); + } + }, + .b_missing => {}, + } + } +}; + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const writeAnyToHasher = bun.writeAnyToHasher; + +const js_ast = bun.js_ast; +const ArrayBinding = js_ast.ArrayBinding; +const Binding = js_ast.Binding; +const Expr = js_ast.Expr; +const ExprNodeIndex = js_ast.ExprNodeIndex; +const Flags = js_ast.Flags; +const Ref = js_ast.Ref; + +const G = js_ast.G; +pub const Class = G.Class; diff --git a/src/ast/Binding.zig b/src/ast/Binding.zig new file mode 100644 index 0000000000..85626ba5cb --- /dev/null +++ b/src/ast/Binding.zig @@ -0,0 +1,165 @@ +loc: logger.Loc, +data: B, + +const Serializable = struct { + type: Tag, + object: string, + value: B, + loc: logger.Loc, +}; + +pub fn jsonStringify(self: *const @This(), writer: anytype) !void { + return try writer.write(Serializable{ .type = std.meta.activeTag(self.data), .object = "binding", .value = self.data, .loc = self.loc }); +} + +pub fn ToExpr(comptime expr_type: type, comptime func_type: anytype) type { + const ExprType = expr_type; + return struct { + context: *ExprType, + allocator: std.mem.Allocator, + pub const Context = @This(); + + pub fn wrapIdentifier(ctx: *const Context, loc: logger.Loc, ref: Ref) Expr { + return func_type(ctx.context, loc, ref); + } + + pub fn init(context: *ExprType) Context { + return Context{ .context = context, .allocator = context.allocator }; + } + }; +} + +pub fn toExpr(binding: *const Binding, wrapper: anytype) Expr { + const loc = binding.loc; + + switch (binding.data) { + .b_missing => { + return Expr{ .data = .{ .e_missing = E.Missing{} }, .loc = loc }; + }, + .b_identifier => |b| { + return wrapper.wrapIdentifier(loc, b.ref); + }, + .b_array => |b| { + var exprs = wrapper.allocator.alloc(Expr, b.items.len) catch unreachable; + var i: usize = 0; + while (i < exprs.len) : (i += 1) { + const item = b.items[i]; + exprs[i] = convert: { + const expr = toExpr(&item.binding, wrapper); + if (b.has_spread and i == exprs.len - 1) { + break :convert Expr.init(E.Spread, E.Spread{ .value = expr }, expr.loc); + } else if (item.default_value) |default| { + break :convert Expr.assign(expr, default); + } else { + break :convert expr; + } + }; + } + + return Expr.init(E.Array, E.Array{ .items = ExprNodeList.init(exprs), .is_single_line = b.is_single_line }, loc); + }, + .b_object => |b| { + const properties = wrapper + .allocator + .alloc(G.Property, b.properties.len) catch unreachable; + for (properties, b.properties) |*property, item| { + property.* = .{ + .flags = item.flags, + .key = item.key, + .kind = if (item.flags.contains(.is_spread)) + .spread + else + .normal, + .value = toExpr(&item.value, wrapper), + .initializer = item.default_value, + }; + } + return Expr.init( + E.Object, + E.Object{ + .properties = G.Property.List.init(properties), + .is_single_line = b.is_single_line, + }, + loc, + ); + }, + } +} + +pub const Tag = enum(u5) { + b_identifier, + b_array, + b_object, + b_missing, + + pub fn jsonStringify(self: @This(), writer: anytype) !void { + return try writer.write(@tagName(self)); + } +}; + +pub var icount: usize = 0; + +pub fn init(t: anytype, loc: logger.Loc) Binding { + icount += 1; + switch (@TypeOf(t)) { + *B.Identifier => { + return Binding{ .loc = loc, .data = B{ .b_identifier = t } }; + }, + *B.Array => { + return Binding{ .loc = loc, .data = B{ .b_array = t } }; + }, + *B.Object => { + return Binding{ .loc = loc, .data = B{ .b_object = t } }; + }, + B.Missing => { + return Binding{ .loc = loc, .data = B{ .b_missing = t } }; + }, + else => { + @compileError("Invalid type passed to Binding.init"); + }, + } +} + +pub fn alloc(allocator: std.mem.Allocator, t: anytype, loc: logger.Loc) Binding { + icount += 1; + switch (@TypeOf(t)) { + B.Identifier => { + const data = allocator.create(B.Identifier) catch unreachable; + data.* = t; + return Binding{ .loc = loc, .data = B{ .b_identifier = data } }; + }, + B.Array => { + const data = allocator.create(B.Array) catch unreachable; + data.* = t; + return Binding{ .loc = loc, .data = B{ .b_array = data } }; + }, + B.Object => { + const data = allocator.create(B.Object) catch unreachable; + data.* = t; + return Binding{ .loc = loc, .data = B{ .b_object = data } }; + }, + B.Missing => { + return Binding{ .loc = loc, .data = B{ .b_missing = .{} } }; + }, + else => { + @compileError("Invalid type passed to Binding.alloc"); + }, + } +} + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const logger = bun.logger; +const string = bun.string; + +const js_ast = bun.js_ast; +const B = js_ast.B; +const Binding = js_ast.Binding; +const E = js_ast.E; +const Expr = js_ast.Expr; +const ExprNodeList = js_ast.ExprNodeList; +const G = js_ast.G; +const Ref = js_ast.Ref; diff --git a/src/ast/BundledAst.zig b/src/ast/BundledAst.zig new file mode 100644 index 0000000000..141909c961 --- /dev/null +++ b/src/ast/BundledAst.zig @@ -0,0 +1,231 @@ +//! Like Ast but slimmer and for bundling only. +//! +//! On Linux, the hottest function in the bundler is: +//! src.multi_array_list.MultiArrayList(src.js_ast.Ast).ensureTotalCapacity +//! https://share.firefox.dev/3NNlRKt +//! +//! So we make a slimmer version of Ast for bundling that doesn't allocate as much memory + +approximate_newline_count: u32 = 0, +nested_scope_slot_counts: SlotCounts = .{}, + +exports_kind: ExportsKind = .none, + +/// These are stored at the AST level instead of on individual AST nodes so +/// they can be manipulated efficiently without a full AST traversal +import_records: ImportRecord.List = .{}, + +hashbang: string = "", +parts: Part.List = .{}, +css: ?*bun.css.BundlerStyleSheet = null, +url_for_css: []const u8 = "", +symbols: Symbol.List = .{}, +module_scope: Scope = .{}, +char_freq: CharFreq = undefined, +exports_ref: Ref = Ref.None, +module_ref: Ref = Ref.None, +wrapper_ref: Ref = Ref.None, +require_ref: Ref = Ref.None, +top_level_await_keyword: logger.Range, +tla_check: TlaCheck = .{}, + +// These are used when bundling. They are filled in during the parser pass +// since we already have to traverse the AST then anyway and the parser pass +// is conveniently fully parallelized. +named_imports: NamedImports = .{}, +named_exports: NamedExports = .{}, +export_star_import_records: []u32 = &.{}, + +top_level_symbols_to_parts: TopLevelSymbolToParts = .{}, + +commonjs_named_exports: CommonJSNamedExports = .{}, + +redirect_import_record_index: u32 = std.math.maxInt(u32), + +/// Only populated when bundling. When --server-components is passed, this +/// will be .browser when it is a client component, and the server's target +/// on the server. +target: bun.options.Target = .browser, + +// const_values: ConstValuesMap = .{}, +ts_enums: Ast.TsEnumsMap = .{}, + +flags: BundledAst.Flags = .{}, + +pub const Flags = packed struct(u8) { + // This is a list of CommonJS features. When a file uses CommonJS features, + // it's not a candidate for "flat bundling" and must be wrapped in its own + // closure. + uses_exports_ref: bool = false, + uses_module_ref: bool = false, + // uses_require_ref: bool = false, + uses_export_keyword: bool = false, + has_char_freq: bool = false, + force_cjs_to_esm: bool = false, + has_lazy_export: bool = false, + commonjs_module_exports_assigned_deoptimized: bool = false, + has_explicit_use_strict_directive: bool = false, +}; + +pub const empty = BundledAst.init(Ast.empty); + +pub fn toAST(this: *const BundledAst) Ast { + return .{ + .approximate_newline_count = this.approximate_newline_count, + .nested_scope_slot_counts = this.nested_scope_slot_counts, + + .exports_kind = this.exports_kind, + + .import_records = this.import_records, + + .hashbang = this.hashbang, + .parts = this.parts, + // This list may be mutated later, so we should store the capacity + .symbols = this.symbols, + .module_scope = this.module_scope, + .char_freq = if (this.flags.has_char_freq) this.char_freq else null, + .exports_ref = this.exports_ref, + .module_ref = this.module_ref, + .wrapper_ref = this.wrapper_ref, + .require_ref = this.require_ref, + .top_level_await_keyword = this.top_level_await_keyword, + + // These are used when bundling. They are filled in during the parser pass + // since we already have to traverse the AST then anyway and the parser pass + // is conveniently fully parallelized. + .named_imports = this.named_imports, + .named_exports = this.named_exports, + .export_star_import_records = this.export_star_import_records, + + .top_level_symbols_to_parts = this.top_level_symbols_to_parts, + + .commonjs_named_exports = this.commonjs_named_exports, + + .redirect_import_record_index = this.redirect_import_record_index, + + .target = this.target, + + // .const_values = this.const_values, + .ts_enums = this.ts_enums, + + .uses_exports_ref = this.flags.uses_exports_ref, + .uses_module_ref = this.flags.uses_module_ref, + // .uses_require_ref = ast.uses_require_ref, + .export_keyword = .{ .len = if (this.flags.uses_export_keyword) 1 else 0, .loc = .{} }, + .force_cjs_to_esm = this.flags.force_cjs_to_esm, + .has_lazy_export = this.flags.has_lazy_export, + .commonjs_module_exports_assigned_deoptimized = this.flags.commonjs_module_exports_assigned_deoptimized, + .directive = if (this.flags.has_explicit_use_strict_directive) "use strict" else null, + }; +} + +pub fn init(ast: Ast) BundledAst { + return .{ + .approximate_newline_count = @as(u32, @truncate(ast.approximate_newline_count)), + .nested_scope_slot_counts = ast.nested_scope_slot_counts, + + .exports_kind = ast.exports_kind, + + .import_records = ast.import_records, + + .hashbang = ast.hashbang, + .parts = ast.parts, + // This list may be mutated later, so we should store the capacity + .symbols = ast.symbols, + .module_scope = ast.module_scope, + .char_freq = ast.char_freq orelse undefined, + .exports_ref = ast.exports_ref, + .module_ref = ast.module_ref, + .wrapper_ref = ast.wrapper_ref, + .require_ref = ast.require_ref, + .top_level_await_keyword = ast.top_level_await_keyword, + // These are used when bundling. They are filled in during the parser pass + // since we already have to traverse the AST then anyway and the parser pass + // is conveniently fully parallelized. + .named_imports = ast.named_imports, + .named_exports = ast.named_exports, + .export_star_import_records = ast.export_star_import_records, + + // .allocator = ast.allocator, + .top_level_symbols_to_parts = ast.top_level_symbols_to_parts, + + .commonjs_named_exports = ast.commonjs_named_exports, + + .redirect_import_record_index = ast.redirect_import_record_index orelse std.math.maxInt(u32), + + .target = ast.target, + + // .const_values = ast.const_values, + .ts_enums = ast.ts_enums, + + .flags = .{ + .uses_exports_ref = ast.uses_exports_ref, + .uses_module_ref = ast.uses_module_ref, + // .uses_require_ref = ast.uses_require_ref, + .uses_export_keyword = ast.export_keyword.len > 0, + .has_char_freq = ast.char_freq != null, + .force_cjs_to_esm = ast.force_cjs_to_esm, + .has_lazy_export = ast.has_lazy_export, + .commonjs_module_exports_assigned_deoptimized = ast.commonjs_module_exports_assigned_deoptimized, + .has_explicit_use_strict_directive = strings.eqlComptime(ast.directive orelse "", "use strict"), + }, + }; +} + +/// TODO: Move this from being done on all parse tasks into the start of the linker. This currently allocates base64 encoding for every small file loaded thing. +pub fn addUrlForCss( + this: *BundledAst, + allocator: std.mem.Allocator, + source: *const logger.Source, + mime_type_: ?[]const u8, + unique_key: ?[]const u8, +) void { + { + const mime_type = if (mime_type_) |m| m else MimeType.byExtension(bun.strings.trimLeadingChar(std.fs.path.extension(source.path.text), '.')).value; + const contents = source.contents; + // TODO: make this configurable + const COPY_THRESHOLD = 128 * 1024; // 128kb + const should_copy = contents.len >= COPY_THRESHOLD and unique_key != null; + if (should_copy) return; + this.url_for_css = url_for_css: { + + // Encode as base64 + const encode_len = bun.base64.encodeLen(contents); + const data_url_prefix_len = "data:".len + mime_type.len + ";base64,".len; + const total_buffer_len = data_url_prefix_len + encode_len; + var encoded = allocator.alloc(u8, total_buffer_len) catch bun.outOfMemory(); + _ = std.fmt.bufPrint(encoded[0..data_url_prefix_len], "data:{s};base64,", .{mime_type}) catch unreachable; + const len = bun.base64.encode(encoded[data_url_prefix_len..], contents); + break :url_for_css encoded[0 .. data_url_prefix_len + len]; + }; + } +} + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const ImportRecord = bun.ImportRecord; +const logger = bun.logger; +const string = bun.string; +const strings = bun.strings; +const MimeType = bun.http.MimeType; + +const js_ast = bun.js_ast; +const BundledAst = js_ast.BundledAst; +const CharFreq = js_ast.CharFreq; +const ExportsKind = js_ast.ExportsKind; +const Part = js_ast.Part; +const Ref = js_ast.Ref; +const Scope = js_ast.Scope; +const SlotCounts = js_ast.SlotCounts; +const Symbol = js_ast.Symbol; +const TlaCheck = js_ast.TlaCheck; + +const Ast = js_ast.Ast; +pub const CommonJSNamedExports = Ast.CommonJSNamedExports; +pub const ConstValuesMap = Ast.ConstValuesMap; +pub const NamedExports = Ast.NamedExports; +pub const NamedImports = Ast.NamedImports; +pub const TopLevelSymbolToParts = Ast.TopLevelSymbolToParts; diff --git a/src/ast/CharFreq.zig b/src/ast/CharFreq.zig new file mode 100644 index 0000000000..e7d3f2baf5 --- /dev/null +++ b/src/ast/CharFreq.zig @@ -0,0 +1,139 @@ +pub const char_freq_count = 64; +pub const CharAndCount = struct { + char: u8 = 0, + count: i32 = 0, + index: usize = 0, + + pub const Array = [char_freq_count]CharAndCount; + + pub fn lessThan(_: void, a: CharAndCount, b: CharAndCount) bool { + if (a.count != b.count) { + return a.count > b.count; + } + + if (a.index != b.index) { + return a.index < b.index; + } + + return a.char < b.char; + } +}; + +const Vector = @Vector(char_freq_count, i32); +const Buffer = [char_freq_count]i32; + +freqs: Buffer align(1) = undefined, + +const scan_big_chunk_size = 32; +pub fn scan(this: *CharFreq, text: string, delta: i32) void { + if (delta == 0) + return; + + if (text.len < scan_big_chunk_size) { + scanSmall(&this.freqs, text, delta); + } else { + scanBig(&this.freqs, text, delta); + } +} + +fn scanBig(out: *align(1) Buffer, text: string, delta: i32) void { + // https://zig.godbolt.org/z/P5dPojWGK + var freqs = out.*; + defer out.* = freqs; + var deltas: [256]i32 = [_]i32{0} ** 256; + var remain = text; + + bun.assert(remain.len >= scan_big_chunk_size); + + const unrolled = remain.len - (remain.len % scan_big_chunk_size); + const remain_end = remain.ptr + unrolled; + var unrolled_ptr = remain.ptr; + remain = remain[unrolled..]; + + while (unrolled_ptr != remain_end) : (unrolled_ptr += scan_big_chunk_size) { + const chunk = unrolled_ptr[0..scan_big_chunk_size].*; + inline for (0..scan_big_chunk_size) |i| { + deltas[@as(usize, chunk[i])] += delta; + } + } + + for (remain) |c| { + deltas[@as(usize, c)] += delta; + } + + freqs[0..26].* = deltas['a' .. 'a' + 26].*; + freqs[26 .. 26 * 2].* = deltas['A' .. 'A' + 26].*; + freqs[26 * 2 .. 62].* = deltas['0' .. '0' + 10].*; + freqs[62] = deltas['_']; + freqs[63] = deltas['$']; +} + +fn scanSmall(out: *align(1) Buffer, text: string, delta: i32) void { + var freqs: [char_freq_count]i32 = out.*; + defer out.* = freqs; + + for (text) |c| { + const i: usize = switch (c) { + 'a'...'z' => @as(usize, @intCast(c)) - 'a', + 'A'...'Z' => @as(usize, @intCast(c)) - ('A' - 26), + '0'...'9' => @as(usize, @intCast(c)) + (53 - '0'), + '_' => 62, + '$' => 63, + else => continue, + }; + freqs[i] += delta; + } +} + +pub fn include(this: *CharFreq, other: CharFreq) void { + // https://zig.godbolt.org/z/Mq8eK6K9s + const left: @Vector(char_freq_count, i32) = this.freqs; + const right: @Vector(char_freq_count, i32) = other.freqs; + + this.freqs = left + right; +} + +pub fn compile(this: *const CharFreq, allocator: std.mem.Allocator) NameMinifier { + const array: CharAndCount.Array = brk: { + var _array: CharAndCount.Array = undefined; + + for (&_array, NameMinifier.default_tail, this.freqs, 0..) |*dest, char, freq, i| { + dest.* = CharAndCount{ + .char = char, + .index = i, + .count = freq, + }; + } + + std.sort.pdq(CharAndCount, &_array, {}, CharAndCount.lessThan); + + break :brk _array; + }; + + var minifier = NameMinifier.init(allocator); + minifier.head.ensureTotalCapacityPrecise(NameMinifier.default_head.len) catch unreachable; + minifier.tail.ensureTotalCapacityPrecise(NameMinifier.default_tail.len) catch unreachable; + // TODO: investigate counting number of < 0 and > 0 and pre-allocating + for (array) |item| { + if (item.char < '0' or item.char > '9') { + minifier.head.append(item.char) catch unreachable; + } + minifier.tail.append(item.char) catch unreachable; + } + + return minifier; +} + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const string = bun.string; + +const js_ast = bun.js_ast; +const CharFreq = js_ast.CharFreq; +const NameMinifier = js_ast.NameMinifier; + +const G = js_ast.G; +pub const Class = G.Class; diff --git a/src/ast/E.zig b/src/ast/E.zig new file mode 100644 index 0000000000..63693549a9 --- /dev/null +++ b/src/ast/E.zig @@ -0,0 +1,1441 @@ +/// This represents an internal property name that can be mangled. The symbol +/// referenced by this expression should be a "SymbolMangledProp" symbol. +pub const NameOfSymbol = struct { + ref: Ref = Ref.None, + + /// If true, a preceding comment contains "@__KEY__" + /// + /// Currently not used + has_property_key_comment: bool = false, +}; + +pub const Array = struct { + items: ExprNodeList = ExprNodeList{}, + comma_after_spread: ?logger.Loc = null, + is_single_line: bool = false, + is_parenthesized: bool = false, + was_originally_macro: bool = false, + close_bracket_loc: logger.Loc = logger.Loc.Empty, + + pub fn push(this: *Array, allocator: std.mem.Allocator, item: Expr) !void { + try this.items.push(allocator, item); + } + + pub inline fn slice(this: Array) []Expr { + return this.items.slice(); + } + + pub fn inlineSpreadOfArrayLiterals( + this: *Array, + allocator: std.mem.Allocator, + estimated_count: usize, + ) !ExprNodeList { + var out = try allocator.alloc( + Expr, + // This over-allocates a little but it's fine + estimated_count + @as(usize, this.items.len), + ); + var remain = out; + for (this.items.slice()) |item| { + switch (item.data) { + .e_spread => |val| { + if (val.value.data == .e_array) { + for (val.value.data.e_array.items.slice()) |inner_item| { + if (inner_item.data == .e_missing) { + remain[0] = Expr.init(E.Undefined, .{}, inner_item.loc); + remain = remain[1..]; + } else { + remain[0] = inner_item; + remain = remain[1..]; + } + } + + // skip empty arrays + // don't include the inlined spread. + continue; + } + // non-arrays are kept in + }, + else => {}, + } + + remain[0] = item; + remain = remain[1..]; + } + + return ExprNodeList.init(out[0 .. out.len - remain.len]); + } + + pub fn toJS(this: @This(), allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) ToJSError!JSC.JSValue { + const items = this.items.slice(); + var array = try JSC.JSValue.createEmptyArray(globalObject, items.len); + array.protect(); + defer array.unprotect(); + for (items, 0..) |expr, j| { + array.putIndex(globalObject, @as(u32, @truncate(j)), try expr.data.toJS(allocator, globalObject)); + } + + return array; + } + + /// Assumes each item in the array is a string + pub fn alphabetizeStrings(this: *Array) void { + if (comptime Environment.allow_assert) { + for (this.items.slice()) |item| { + bun.assert(item.data == .e_string); + } + } + std.sort.pdq(Expr, this.items.slice(), {}, Sorter.isLessThan); + } + + const Sorter = struct { + pub fn isLessThan(ctx: void, lhs: Expr, rhs: Expr) bool { + return strings.cmpStringsAsc(ctx, lhs.data.e_string.data, rhs.data.e_string.data); + } + }; +}; + +pub const Unary = struct { + op: Op.Code, + value: ExprNodeIndex, +}; + +pub const Binary = struct { + left: ExprNodeIndex, + right: ExprNodeIndex, + op: Op.Code, +}; + +pub const Boolean = struct { + value: bool, + pub fn toJS(this: @This(), ctx: *JSC.JSGlobalObject) JSC.C.JSValueRef { + return JSC.C.JSValueMakeBoolean(ctx, this.value); + } +}; +pub const Super = struct {}; +pub const Null = struct {}; +pub const This = struct {}; +pub const Undefined = struct {}; +pub const New = struct { + target: ExprNodeIndex, + args: ExprNodeList = ExprNodeList{}, + + // True if there is a comment containing "@__PURE__" or "#__PURE__" preceding + // this call expression. See the comment inside ECall for more details. + can_be_unwrapped_if_unused: bool = false, + + close_parens_loc: logger.Loc, +}; +pub const NewTarget = struct { + range: logger.Range, +}; +pub const ImportMeta = struct {}; +pub const ImportMetaMain = struct { + /// If we want to print `!import.meta.main`, set this flag to true + /// instead of wrapping in a unary not. This way, the printer can easily + /// print `require.main != module` instead of `!(require.main == module)` + inverted: bool = false, +}; + +pub const Special = union(enum) { + /// emits `exports` or `module.exports` depending on `commonjs_named_exports_deoptimized` + module_exports, + /// `import.meta.hot` + hot_enabled, + /// Acts as .e_undefined, but allows property accesses to the rest of the HMR API. + hot_disabled, + /// `import.meta.hot.data` when HMR is enabled. Not reachable when it is disabled. + hot_data, + /// `import.meta.hot.accept` when HMR is enabled. Truthy. + hot_accept, + /// Converted from `hot_accept` to this in js_parser.zig when it is + /// passed strings. Printed as `hmr.hot.acceptSpecifiers` + hot_accept_visited, + /// Prints the resolved specifier string for an import record. + resolved_specifier_string: ImportRecord.Index, +}; + +pub const Call = struct { + // Node: + target: ExprNodeIndex, + args: ExprNodeList = ExprNodeList{}, + optional_chain: ?OptionalChain = null, + is_direct_eval: bool = false, + close_paren_loc: logger.Loc = logger.Loc.Empty, + + // True if there is a comment containing "@__PURE__" or "#__PURE__" preceding + // this call expression. This is an annotation used for tree shaking, and + // means that the call can be removed if it's unused. It does not mean the + // call is pure (e.g. it may still return something different if called twice). + // + // Note that the arguments are not considered to be part of the call. If the + // call itself is removed due to this annotation, the arguments must remain + // if they have side effects. + can_be_unwrapped_if_unused: bool = false, + + // Used when printing to generate the source prop on the fly + was_jsx_element: bool = false, + + pub fn hasSameFlagsAs(a: *Call, b: *Call) bool { + return (a.optional_chain == b.optional_chain and + a.is_direct_eval == b.is_direct_eval and + a.can_be_unwrapped_if_unused == b.can_be_unwrapped_if_unused); + } +}; + +pub const Dot = struct { + // target is Node + target: ExprNodeIndex, + name: string, + name_loc: logger.Loc, + optional_chain: ?OptionalChain = null, + + // If true, this property access is known to be free of side-effects. That + // means it can be removed if the resulting value isn't used. + can_be_removed_if_unused: bool = false, + + // If true, this property access is a function that, when called, can be + // unwrapped if the resulting value is unused. Unwrapping means discarding + // the call target but keeping any arguments with side effects. + call_can_be_unwrapped_if_unused: bool = false, + + pub fn hasSameFlagsAs(a: *Dot, b: *Dot) bool { + return (a.optional_chain == b.optional_chain and + a.is_direct_eval == b.is_direct_eval and + a.can_be_removed_if_unused == b.can_be_removed_if_unused and a.call_can_be_unwrapped_if_unused == b.call_can_be_unwrapped_if_unused); + } +}; + +pub const Index = struct { + index: ExprNodeIndex, + target: ExprNodeIndex, + optional_chain: ?OptionalChain = null, + + pub fn hasSameFlagsAs(a: *E.Index, b: *E.Index) bool { + return (a.optional_chain == b.optional_chain); + } +}; + +pub const Arrow = struct { + args: []G.Arg = &[_]G.Arg{}, + body: G.FnBody, + + is_async: bool = false, + has_rest_arg: bool = false, + prefer_expr: bool = false, // Use shorthand if true and "Body" is a single return statement + + pub const noop_return_undefined: Arrow = .{ + .args = &.{}, + .body = .{ + .loc = .Empty, + .stmts = &.{}, + }, + }; +}; + +pub const Function = struct { func: G.Fn }; + +pub const Identifier = struct { + ref: Ref = Ref.None, + + // If we're inside a "with" statement, this identifier may be a property + // access. In that case it would be incorrect to remove this identifier since + // the property access may be a getter or setter with side effects. + must_keep_due_to_with_stmt: bool = false, + + // If true, this identifier is known to not have a side effect (i.e. to not + // throw an exception) when referenced. If false, this identifier may or + // not have side effects when referenced. This is used to allow the removal + // of known globals such as "Object" if they aren't used. + can_be_removed_if_unused: bool = false, + + // If true, this identifier represents a function that, when called, can be + // unwrapped if the resulting value is unused. Unwrapping means discarding + // the call target but keeping any arguments with side effects. + call_can_be_unwrapped_if_unused: bool = false, + + pub inline fn init(ref: Ref) Identifier { + return Identifier{ + .ref = ref, + .must_keep_due_to_with_stmt = false, + .can_be_removed_if_unused = false, + .call_can_be_unwrapped_if_unused = false, + }; + } +}; + +/// This is similar to an `Identifier` but it represents a reference to an ES6 +/// import item. +/// +/// Depending on how the code is linked, the file containing this EImportIdentifier +/// may or may not be in the same module group as the file it was imported from. +/// +/// If it's the same module group than we can just merge the import item symbol +/// with the corresponding symbol that was imported, effectively renaming them +/// to be the same thing and statically binding them together. +/// +/// But if it's a different module group, then the import must be dynamically +/// evaluated using a property access off the corresponding namespace symbol, +/// which represents the result of a require() call. +/// +/// It's stored as a separate type so it's not easy to confuse with a plain +/// identifier. For example, it'd be bad if code trying to convert "{x: x}" into +/// "{x}" shorthand syntax wasn't aware that the "x" in this case is actually +/// "{x: importedNamespace.x}". This separate type forces code to opt-in to +/// doing this instead of opt-out. +pub const ImportIdentifier = struct { + ref: Ref = Ref.None, + + /// If true, this was originally an identifier expression such as "foo". If + /// false, this could potentially have been a member access expression such + /// as "ns.foo" off of an imported namespace object. + was_originally_identifier: bool = false, +}; + +/// This is a dot expression on exports, such as `exports.`. It is given +/// it's own AST node to allow CommonJS unwrapping, in which this can just be +/// the identifier in the Ref +pub const CommonJSExportIdentifier = struct { + ref: Ref = Ref.None, + base: Base = .exports, + + /// The original variant of the dot expression must be known so that in the case that we + /// - fail to convert this to ESM + /// - ALSO see an assignment to `module.exports` (commonjs_module_exports_assigned_deoptimized) + /// It must be known if `exports` or `module.exports` was written in source + /// code, as the distinction will alter behavior. The fixup happens in the printer when + /// printing this node. + pub const Base = enum { + exports, + module_dot_exports, + }; +}; + +// This is similar to EIdentifier but it represents class-private fields and +// methods. It can be used where computed properties can be used, such as +// EIndex and Property. +pub const PrivateIdentifier = struct { + ref: Ref, +}; + +/// In development mode, the new JSX transform has a few special props +/// - `React.jsxDEV(type, arguments, key, isStaticChildren, source, self)` +/// - `arguments`: +/// ```{ ...props, children: children, }``` +/// - `source`: https://github.com/babel/babel/blob/ef87648f3f05ccc393f89dea7d4c7c57abf398ce/packages/babel-plugin-transform-react-jsx-source/src/index.js#L24-L48 +/// ```{ +/// fileName: string | null, +/// columnNumber: number | null, +/// lineNumber: number | null, +/// }``` +/// - `children`: +/// - static the function is React.jsxsDEV, "jsxs" instead of "jsx" +/// - one child? the function is React.jsxDEV, +/// - no children? the function is React.jsxDEV and children is an empty array. +/// `isStaticChildren`: https://github.com/facebook/react/blob/4ca62cac45c288878d2532e5056981d177f9fdac/packages/react/src/jsx/ReactJSXElementValidator.js#L369-L384 +/// This flag means children is an array of JSX Elements literals. +/// The documentation on this is sparse, but it appears that +/// React just calls Object.freeze on the children array. +/// Object.freeze, historically, is quite a bit slower[0] than just not doing that. +/// Given that...I am choosing to always pass "false" to this. +/// This also skips extra state that we'd need to track. +/// If React Fast Refresh ends up using this later, then we can revisit this decision. +/// [0]: https://github.com/automerge/automerge/issues/177 +pub const JSXElement = struct { + /// JSX tag name + ///
=> E.String.init("div") + /// => E.Identifier{.ref = symbolPointingToMyComponent } + /// null represents a fragment + tag: ?ExprNodeIndex = null, + + /// JSX props + properties: G.Property.List = G.Property.List{}, + + /// JSX element children
{this_is_a_child_element}
+ children: ExprNodeList = ExprNodeList{}, + + // needed to make sure parse and visit happen in the same order + key_prop_index: i32 = -1, + + flags: Flags.JSXElement.Bitset = Flags.JSXElement.Bitset{}, + + close_tag_loc: logger.Loc = logger.Loc.Empty, + + pub const SpecialProp = enum { + __self, // old react transform used this as a prop + __source, + key, + ref, + any, + + pub const Map = ComptimeStringMap(SpecialProp, .{ + .{ "__self", .__self }, + .{ "__source", .__source }, + .{ "key", .key }, + .{ "ref", .ref }, + }); + }; +}; + +pub const Missing = struct { + pub fn jsonStringify(_: *const @This(), writer: anytype) !void { + return try writer.write(null); + } +}; + +pub const Number = struct { + value: f64, + + const double_digit = [_]string{ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "100" }; + const neg_double_digit = [_]string{ "-0", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9", "-10", "-11", "-12", "-13", "-14", "-15", "-16", "-17", "-18", "-19", "-20", "-21", "-22", "-23", "-24", "-25", "-26", "-27", "-28", "-29", "-30", "-31", "-32", "-33", "-34", "-35", "-36", "-37", "-38", "-39", "-40", "-41", "-42", "-43", "-44", "-45", "-46", "-47", "-48", "-49", "-50", "-51", "-52", "-53", "-54", "-55", "-56", "-57", "-58", "-59", "-60", "-61", "-62", "-63", "-64", "-65", "-66", "-67", "-68", "-69", "-70", "-71", "-72", "-73", "-74", "-75", "-76", "-77", "-78", "-79", "-80", "-81", "-82", "-83", "-84", "-85", "-86", "-87", "-88", "-89", "-90", "-91", "-92", "-93", "-94", "-95", "-96", "-97", "-98", "-99", "-100" }; + + /// String concatenation with numbers is required by the TypeScript compiler for + /// "constant expression" handling in enums. We can match the behavior of a JS VM + /// by calling out to the APIs in WebKit which are responsible for this operation. + /// + /// This can return `null` in wasm builds to avoid linking JSC + pub fn toString(this: Number, allocator: std.mem.Allocator) ?string { + return toStringFromF64(this.value, allocator); + } + + pub fn toStringFromF64(value: f64, allocator: std.mem.Allocator) ?string { + if (value == @trunc(value) and (value < std.math.maxInt(i32) and value > std.math.minInt(i32))) { + const int_value = @as(i64, @intFromFloat(value)); + const abs = @as(u64, @intCast(@abs(int_value))); + + // do not allocate for a small set of constant numbers: -100 through 100 + if (abs < double_digit.len) { + return if (int_value < 0) + neg_double_digit[abs] + else + double_digit[abs]; + } + + return std.fmt.allocPrint(allocator, "{d}", .{@as(i32, @intCast(int_value))}) catch return null; + } + + if (std.math.isNan(value)) { + return "NaN"; + } + + if (std.math.isNegativeInf(value)) { + return "-Infinity"; + } + + if (std.math.isInf(value)) { + return "Infinity"; + } + + if (Environment.isNative) { + var buf: [124]u8 = undefined; + return allocator.dupe(u8, bun.fmt.FormatDouble.dtoa(&buf, value)) catch bun.outOfMemory(); + } else { + // do not attempt to implement the spec here, it would be error prone. + } + + return null; + } + + pub inline fn toU64(self: Number) u64 { + return self.to(u64); + } + + pub inline fn toUsize(self: Number) usize { + return self.to(usize); + } + + pub inline fn toU32(self: Number) u32 { + return self.to(u32); + } + + pub inline fn toU16(self: Number) u16 { + return self.to(u16); + } + + pub fn to(self: Number, comptime T: type) T { + return @as(T, @intFromFloat(@min(@max(@trunc(self.value), 0), comptime @min(std.math.floatMax(f64), std.math.maxInt(T))))); + } + + pub fn jsonStringify(self: *const Number, writer: anytype) !void { + return try writer.write(self.value); + } + + pub fn toJS(this: @This()) JSC.JSValue { + return JSC.JSValue.jsNumber(this.value); + } +}; + +pub const BigInt = struct { + value: string, + + pub var empty = BigInt{ .value = "" }; + + pub fn jsonStringify(self: *const @This(), writer: anytype) !void { + return try writer.write(self.value); + } + + pub fn toJS(_: @This()) JSC.JSValue { + // TODO: + return JSC.JSValue.jsNumber(0); + } +}; + +pub const Object = struct { + properties: G.Property.List = G.Property.List{}, + comma_after_spread: ?logger.Loc = null, + is_single_line: bool = false, + is_parenthesized: bool = false, + was_originally_macro: bool = false, + + close_brace_loc: logger.Loc = logger.Loc.Empty, + + // used in TOML parser to merge properties + pub const Rope = struct { + head: Expr, + next: ?*Rope = null, + pub fn append(this: *Rope, expr: Expr, allocator: std.mem.Allocator) OOM!*Rope { + if (this.next) |next| { + return try next.append(expr, allocator); + } + + const rope = try allocator.create(Rope); + rope.* = .{ .head = expr }; + this.next = rope; + return rope; + } + }; + + pub fn get(self: *const Object, key: string) ?Expr { + return if (asProperty(self, key)) |query| query.expr else @as(?Expr, null); + } + + pub fn toJS(this: *Object, allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) ToJSError!JSC.JSValue { + var obj = JSC.JSValue.createEmptyObject(globalObject, this.properties.len); + obj.protect(); + defer obj.unprotect(); + const props: []const G.Property = this.properties.slice(); + for (props) |prop| { + if (prop.kind != .normal or prop.class_static_block != null or prop.key == null or prop.value == null) { + return error.@"Cannot convert argument type to JS"; + } + const key = try prop.key.?.data.toJS(allocator, globalObject); + const value = try prop.value.?.toJS(allocator, globalObject); + try obj.putToPropertyKey(globalObject, key, value); + } + + return obj; + } + + pub fn put(self: *Object, allocator: std.mem.Allocator, key: string, expr: Expr) !void { + if (asProperty(self, key)) |query| { + self.properties.ptr[query.i].value = expr; + } else { + try self.properties.push(allocator, .{ + .key = Expr.init(E.String, E.String.init(key), expr.loc), + .value = expr, + }); + } + } + + pub fn putString(self: *Object, allocator: std.mem.Allocator, key: string, value: string) !void { + return try put(self, allocator, key, Expr.init(E.String, E.String.init(value), logger.Loc.Empty)); + } + + pub const SetError = error{ OutOfMemory, Clobber }; + + pub fn set(self: *const Object, key: Expr, allocator: std.mem.Allocator, value: Expr) SetError!void { + if (self.hasProperty(key.data.e_string.data)) return error.Clobber; + try self.properties.push(allocator, .{ + .key = key, + .value = value, + }); + } + + pub const RopeQuery = struct { + expr: Expr, + rope: *const Rope, + }; + + // this is terribly, shamefully slow + pub fn setRope(self: *Object, rope: *const Rope, allocator: std.mem.Allocator, value: Expr) SetError!void { + if (self.get(rope.head.data.e_string.data)) |existing| { + switch (existing.data) { + .e_array => |array| { + if (rope.next == null) { + try array.push(allocator, value); + return; + } + + if (array.items.last()) |last| { + if (last.data != .e_object) { + return error.Clobber; + } + + try last.data.e_object.setRope(rope.next.?, allocator, value); + return; + } + + try array.push(allocator, value); + return; + }, + .e_object => |object| { + if (rope.next != null) { + try object.setRope(rope.next.?, allocator, value); + return; + } + + return error.Clobber; + }, + else => { + return error.Clobber; + }, + } + } + + var value_ = value; + if (rope.next) |next| { + var obj = Expr.init(E.Object, E.Object{ .properties = .{} }, rope.head.loc); + try obj.data.e_object.setRope(next, allocator, value); + value_ = obj; + } + + try self.properties.push(allocator, .{ + .key = rope.head, + .value = value_, + }); + } + + pub fn getOrPutObject(self: *Object, rope: *const Rope, allocator: std.mem.Allocator) SetError!Expr { + if (self.get(rope.head.data.e_string.data)) |existing| { + switch (existing.data) { + .e_array => |array| { + if (rope.next == null) { + return error.Clobber; + } + + if (array.items.last()) |last| { + if (last.data != .e_object) { + return error.Clobber; + } + + return try last.data.e_object.getOrPutObject(rope.next.?, allocator); + } + + return error.Clobber; + }, + .e_object => |object| { + if (rope.next != null) { + return try object.getOrPutObject(rope.next.?, allocator); + } + + // success + return existing; + }, + else => { + return error.Clobber; + }, + } + } + + if (rope.next) |next| { + var obj = Expr.init(E.Object, E.Object{ .properties = .{} }, rope.head.loc); + const out = try obj.data.e_object.getOrPutObject(next, allocator); + try self.properties.push(allocator, .{ + .key = rope.head, + .value = obj, + }); + return out; + } + + const out = Expr.init(E.Object, E.Object{}, rope.head.loc); + try self.properties.push(allocator, .{ + .key = rope.head, + .value = out, + }); + return out; + } + + pub fn getOrPutArray(self: *Object, rope: *const Rope, allocator: std.mem.Allocator) SetError!Expr { + if (self.get(rope.head.data.e_string.data)) |existing| { + switch (existing.data) { + .e_array => |array| { + if (rope.next == null) { + return existing; + } + + if (array.items.last()) |last| { + if (last.data != .e_object) { + return error.Clobber; + } + + return try last.data.e_object.getOrPutArray(rope.next.?, allocator); + } + + return error.Clobber; + }, + .e_object => |object| { + if (rope.next == null) { + return error.Clobber; + } + + return try object.getOrPutArray(rope.next.?, allocator); + }, + else => { + return error.Clobber; + }, + } + } + + if (rope.next) |next| { + var obj = Expr.init(E.Object, E.Object{ .properties = .{} }, rope.head.loc); + const out = try obj.data.e_object.getOrPutArray(next, allocator); + try self.properties.push(allocator, .{ + .key = rope.head, + .value = obj, + }); + return out; + } + + const out = Expr.init(E.Array, E.Array{}, rope.head.loc); + try self.properties.push(allocator, .{ + .key = rope.head, + .value = out, + }); + return out; + } + + pub fn hasProperty(obj: *const Object, name: string) bool { + for (obj.properties.slice()) |prop| { + const key = prop.key orelse continue; + if (std.meta.activeTag(key.data) != .e_string) continue; + if (key.data.e_string.eql(string, name)) return true; + } + return false; + } + + pub fn asProperty(obj: *const Object, name: string) ?Expr.Query { + for (obj.properties.slice(), 0..) |prop, i| { + const value = prop.value orelse continue; + const key = prop.key orelse continue; + if (std.meta.activeTag(key.data) != .e_string) continue; + const key_str = key.data.e_string; + if (key_str.eql(string, name)) { + return Expr.Query{ + .expr = value, + .loc = key.loc, + .i = @as(u32, @truncate(i)), + }; + } + } + + return null; + } + + /// Assumes each key in the property is a string + pub fn alphabetizeProperties(this: *Object) void { + if (comptime Environment.isDebug) { + for (this.properties.slice()) |prop| { + bun.assert(prop.key.?.data == .e_string); + } + } + std.sort.pdq(G.Property, this.properties.slice(), {}, Sorter.isLessThan); + } + + pub fn packageJSONSort(this: *Object) void { + std.sort.pdq(G.Property, this.properties.slice(), {}, PackageJSONSort.Fields.isLessThan); + } + + const PackageJSONSort = struct { + const Fields = enum(u8) { + name = 0, + version = 1, + author = 2, + repository = 3, + config = 4, + main = 5, + module = 6, + dependencies = 7, + devDependencies = 8, + optionalDependencies = 9, + peerDependencies = 10, + exports = 11, + __fake = 12, + + pub const Map = ComptimeStringMap(Fields, .{ + .{ "name", Fields.name }, + .{ "version", Fields.version }, + .{ "author", Fields.author }, + .{ "repository", Fields.repository }, + .{ "config", Fields.config }, + .{ "main", Fields.main }, + .{ "module", Fields.module }, + .{ "dependencies", Fields.dependencies }, + .{ "devDependencies", Fields.devDependencies }, + .{ "optionalDependencies", Fields.optionalDependencies }, + .{ "peerDependencies", Fields.peerDependencies }, + .{ "exports", Fields.exports }, + }); + + pub fn isLessThan(ctx: void, lhs: G.Property, rhs: G.Property) bool { + var lhs_key_size: u8 = @intFromEnum(Fields.__fake); + var rhs_key_size: u8 = @intFromEnum(Fields.__fake); + + if (lhs.key != null and lhs.key.?.data == .e_string) { + lhs_key_size = @intFromEnum(Map.get(lhs.key.?.data.e_string.data) orelse Fields.__fake); + } + + if (rhs.key != null and rhs.key.?.data == .e_string) { + rhs_key_size = @intFromEnum(Map.get(rhs.key.?.data.e_string.data) orelse Fields.__fake); + } + + return switch (std.math.order(lhs_key_size, rhs_key_size)) { + .lt => true, + .gt => false, + .eq => strings.cmpStringsAsc(ctx, lhs.key.?.data.e_string.data, rhs.key.?.data.e_string.data), + }; + } + }; + }; + + const Sorter = struct { + pub fn isLessThan(ctx: void, lhs: G.Property, rhs: G.Property) bool { + return strings.cmpStringsAsc(ctx, lhs.key.?.data.e_string.data, rhs.key.?.data.e_string.data); + } + }; +}; + +pub const Spread = struct { value: ExprNodeIndex }; + +/// JavaScript string literal type +pub const String = struct { + // A version of this where `utf8` and `value` are stored in a packed union, with len as a single u32 was attempted. + // It did not improve benchmarks. Neither did converting this from a heap-allocated type to a stack-allocated type. + // TODO: change this to *const anyopaque and change all uses to either .slice8() or .slice16() + data: []const u8 = "", + prefer_template: bool = false, + + // A very simple rope implementation + // We only use this for string folding, so this is kind of overkill + // We don't need to deal with substrings + next: ?*String = null, + end: ?*String = null, + rope_len: u32 = 0, + is_utf16: bool = false, + + pub fn isIdentifier(this: *String, allocator: std.mem.Allocator) bool { + if (!this.isUTF8()) { + return bun.js_lexer.isIdentifierUTF16(this.slice16()); + } + + return bun.js_lexer.isIdentifier(this.slice(allocator)); + } + + pub const class = E.String{ .data = "class" }; + + pub fn push(this: *String, other: *String) void { + bun.assert(this.isUTF8()); + bun.assert(other.isUTF8()); + + if (other.rope_len == 0) { + other.rope_len = @truncate(other.data.len); + } + + if (this.rope_len == 0) { + this.rope_len = @truncate(this.data.len); + } + + this.rope_len += other.rope_len; + if (this.next == null) { + this.next = other; + this.end = other; + } else { + var end = this.end.?; + while (end.next != null) end = end.end.?; + end.next = other; + this.end = other; + } + } + + /// Cloning the rope string is rarely needed, see `foldStringAddition`'s + /// comments and the 'edgecase/EnumInliningRopeStringPoison' test + pub fn cloneRopeNodes(s: String) String { + var root = s; + + if (root.next != null) { + var current: ?*String = &root; + while (true) { + const node = current.?; + if (node.next) |next| { + node.next = Expr.Data.Store.append(String, next.*); + current = node.next; + } else { + root.end = node; + break; + } + } + } + + return root; + } + + pub fn toUTF8(this: *String, allocator: std.mem.Allocator) !void { + if (!this.is_utf16) return; + this.data = try strings.toUTF8Alloc(allocator, this.slice16()); + this.is_utf16 = false; + } + + pub fn init(value: anytype) String { + const Value = @TypeOf(value); + if (Value == []u16 or Value == []const u16) { + return .{ + .data = @as([*]const u8, @ptrCast(value.ptr))[0..value.len], + .is_utf16 = true, + }; + } + + return .{ .data = value }; + } + + /// E.String containing non-ascii characters may not fully work. + /// https://github.com/oven-sh/bun/issues/11963 + /// More investigation is needed. + pub fn initReEncodeUTF8(utf8: []const u8, allocator: std.mem.Allocator) String { + return if (bun.strings.isAllASCII(utf8)) + init(utf8) + else + init(bun.strings.toUTF16AllocForReal(allocator, utf8, false, false) catch bun.outOfMemory()); + } + + pub fn slice8(this: *const String) []const u8 { + bun.assert(!this.is_utf16); + return this.data; + } + + pub fn slice16(this: *const String) []const u16 { + bun.assert(this.is_utf16); + return @as([*]const u16, @ptrCast(@alignCast(this.data.ptr)))[0..this.data.len]; + } + + pub fn resolveRopeIfNeeded(this: *String, allocator: std.mem.Allocator) void { + if (this.next == null or !this.isUTF8()) return; + var bytes = std.ArrayList(u8).initCapacity(allocator, this.rope_len) catch bun.outOfMemory(); + + bytes.appendSliceAssumeCapacity(this.data); + var str = this.next; + while (str) |part| { + bytes.appendSlice(part.data) catch bun.outOfMemory(); + str = part.next; + } + this.data = bytes.items; + this.next = null; + } + + pub fn slice(this: *String, allocator: std.mem.Allocator) []const u8 { + this.resolveRopeIfNeeded(allocator); + return this.string(allocator) catch bun.outOfMemory(); + } + + pub var empty = String{}; + pub var @"true" = String{ .data = "true" }; + pub var @"false" = String{ .data = "false" }; + pub var @"null" = String{ .data = "null" }; + pub var @"undefined" = String{ .data = "undefined" }; + + pub fn clone(str: *const String, allocator: std.mem.Allocator) !String { + return String{ + .data = try allocator.dupe(u8, str.data), + .prefer_template = str.prefer_template, + .is_utf16 = !str.isUTF8(), + }; + } + + pub fn cloneSliceIfNecessary(str: *const String, allocator: std.mem.Allocator) !bun.string { + if (str.isUTF8()) { + return allocator.dupe(u8, str.string(allocator) catch unreachable); + } + + return str.string(allocator); + } + + pub fn javascriptLength(s: *const String) ?u32 { + if (s.rope_len > 0) { + // We only support ascii ropes for now + return s.rope_len; + } + + if (s.isUTF8()) { + if (!strings.isAllASCII(s.data)) { + return null; + } + return @truncate(s.data.len); + } + + return @truncate(s.slice16().len); + } + + pub inline fn len(s: *const String) usize { + return if (s.rope_len > 0) s.rope_len else s.data.len; + } + + pub inline fn isUTF8(s: *const String) bool { + return !s.is_utf16; + } + + pub inline fn isBlank(s: *const String) bool { + return s.len() == 0; + } + + pub inline fn isPresent(s: *const String) bool { + return s.len() > 0; + } + + pub fn eql(s: *const String, comptime _t: type, other: anytype) bool { + if (s.isUTF8()) { + switch (_t) { + @This() => { + if (other.isUTF8()) { + return strings.eqlLong(s.data, other.data, true); + } else { + return strings.utf16EqlString(other.slice16(), s.data); + } + }, + bun.string => { + return strings.eqlLong(s.data, other, true); + }, + []u16, []const u16 => { + return strings.utf16EqlString(other, s.data); + }, + else => { + @compileError("Invalid type"); + }, + } + } else { + switch (_t) { + @This() => { + if (other.isUTF8()) { + return strings.utf16EqlString(s.slice16(), other.data); + } else { + return std.mem.eql(u16, other.slice16(), s.slice16()); + } + }, + bun.string => { + return strings.utf16EqlString(s.slice16(), other); + }, + []u16, []const u16 => { + return std.mem.eql(u16, other.slice16(), s.slice16()); + }, + else => { + @compileError("Invalid type"); + }, + } + } + } + + pub fn eqlComptime(s: *const String, comptime value: []const u8) bool { + bun.assert(s.next == null); + return if (s.isUTF8()) + strings.eqlComptime(s.data, value) + else + strings.eqlComptimeUTF16(s.slice16(), value); + } + + pub fn hasPrefixComptime(s: *const String, comptime value: anytype) bool { + if (s.data.len < value.len) + return false; + + return if (s.isUTF8()) + strings.eqlComptime(s.data[0..value.len], value) + else + strings.eqlComptimeUTF16(s.slice16()[0..value.len], value); + } + + pub fn string(s: *const String, allocator: std.mem.Allocator) OOM!bun.string { + if (s.isUTF8()) { + return s.data; + } else { + return strings.toUTF8Alloc(allocator, s.slice16()); + } + } + + pub fn stringZ(s: *const String, allocator: std.mem.Allocator) OOM!bun.stringZ { + if (s.isUTF8()) { + return allocator.dupeZ(u8, s.data); + } else { + return strings.toUTF8AllocZ(allocator, s.slice16()); + } + } + + pub fn stringCloned(s: *const String, allocator: std.mem.Allocator) OOM!bun.string { + if (s.isUTF8()) { + return allocator.dupe(u8, s.data); + } else { + return strings.toUTF8Alloc(allocator, s.slice16()); + } + } + + pub fn hash(s: *const String) u64 { + if (s.isBlank()) return 0; + + if (s.isUTF8()) { + // hash utf-8 + return bun.hash(s.data); + } else { + // hash utf-16 + return bun.hash(@as([*]const u8, @ptrCast(s.slice16().ptr))[0 .. s.slice16().len * 2]); + } + } + + pub fn toJS(s: *String, allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) !JSC.JSValue { + s.resolveRopeIfNeeded(allocator); + if (!s.isPresent()) { + var emp = bun.String.empty; + return emp.toJS(globalObject); + } + + if (s.isUTF8()) { + if (try strings.toUTF16Alloc(allocator, s.slice8(), false, false)) |utf16| { + var out, const chars = bun.String.createUninitialized(.utf16, utf16.len); + @memcpy(chars, utf16); + return out.transferToJS(globalObject); + } else { + var out, const chars = bun.String.createUninitialized(.latin1, s.slice8().len); + @memcpy(chars, s.slice8()); + return out.transferToJS(globalObject); + } + } else { + var out, const chars = bun.String.createUninitialized(.utf16, s.slice16().len); + @memcpy(chars, s.slice16()); + return out.transferToJS(globalObject); + } + } + + pub fn toZigString(s: *String, allocator: std.mem.Allocator) JSC.ZigString { + if (s.isUTF8()) { + return JSC.ZigString.fromUTF8(s.slice(allocator)); + } else { + return JSC.ZigString.initUTF16(s.slice16()); + } + } + + pub fn format(s: String, comptime fmt: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + comptime bun.assert(fmt.len == 0); + + try writer.writeAll("E.String"); + if (s.next == null) { + try writer.writeAll("("); + if (s.isUTF8()) { + try writer.print("\"{s}\"", .{s.data}); + } else { + try writer.print("\"{}\"", .{bun.fmt.utf16(s.slice16())}); + } + try writer.writeAll(")"); + } else { + try writer.writeAll("(rope: ["); + var it: ?*const String = &s; + while (it) |part| { + if (part.isUTF8()) { + try writer.print("\"{s}\"", .{part.data}); + } else { + try writer.print("\"{}\"", .{bun.fmt.utf16(part.slice16())}); + } + it = part.next; + if (it != null) try writer.writeAll(" "); + } + try writer.writeAll("])"); + } + } + + pub fn jsonStringify(s: *const String, writer: anytype) !void { + var buf = [_]u8{0} ** 4096; + var i: usize = 0; + for (s.slice16()) |char| { + buf[i] = @as(u8, @intCast(char)); + i += 1; + if (i >= 4096) { + break; + } + } + + return try writer.write(buf[0..i]); + } +}; + +// value is in the Node +pub const TemplatePart = struct { + value: ExprNodeIndex, + tail_loc: logger.Loc, + tail: Template.Contents, +}; + +pub const Template = struct { + tag: ?ExprNodeIndex = null, + parts: []TemplatePart = &.{}, + head: Contents, + + pub const Contents = union(Tag) { + cooked: E.String, + raw: string, + + const Tag = enum { + cooked, + raw, + }; + + pub fn isUTF8(contents: Contents) bool { + return contents == .cooked and contents.cooked.isUTF8(); + } + }; + + /// "`a${'b'}c`" => "`abc`" + pub fn fold( + this: *Template, + allocator: std.mem.Allocator, + loc: logger.Loc, + ) Expr { + if (this.tag != null or (this.head == .cooked and !this.head.cooked.isUTF8())) { + // we only fold utf-8/ascii for now + return Expr{ + .data = .{ .e_template = this }, + .loc = loc, + }; + } + + bun.assert(this.head == .cooked); + + if (this.parts.len == 0) { + return Expr.init(E.String, this.head.cooked, loc); + } + + var parts = std.ArrayList(TemplatePart).initCapacity(allocator, this.parts.len) catch unreachable; + var head = Expr.init(E.String, this.head.cooked, loc); + for (this.parts) |part_src| { + var part = part_src; + bun.assert(part.tail == .cooked); + + part.value = part.value.unwrapInlined(); + + switch (part.value.data) { + .e_number => { + if (part.value.data.e_number.toString(allocator)) |s| { + part.value = Expr.init(E.String, E.String.init(s), part.value.loc); + } + }, + .e_null => { + part.value = Expr.init(E.String, E.String.init("null"), part.value.loc); + }, + .e_boolean => { + part.value = Expr.init(E.String, E.String.init(if (part.value.data.e_boolean.value) + "true" + else + "false"), part.value.loc); + }, + .e_undefined => { + part.value = Expr.init(E.String, E.String.init("undefined"), part.value.loc); + }, + .e_big_int => |value| { + part.value = Expr.init(E.String, E.String.init(value.value), part.value.loc); + }, + else => {}, + } + + if (part.value.data == .e_string and part.tail.cooked.isUTF8() and part.value.data.e_string.isUTF8()) { + if (parts.items.len == 0) { + if (part.value.data.e_string.len() > 0) { + head.data.e_string.push(Expr.init(E.String, part.value.data.e_string.*, logger.Loc.Empty).data.e_string); + } + + if (part.tail.cooked.len() > 0) { + head.data.e_string.push(Expr.init(E.String, part.tail.cooked, part.tail_loc).data.e_string); + } + + continue; + } else { + var prev_part = &parts.items[parts.items.len - 1]; + bun.assert(prev_part.tail == .cooked); + + if (prev_part.tail.cooked.isUTF8()) { + if (part.value.data.e_string.len() > 0) { + prev_part.tail.cooked.push(Expr.init(E.String, part.value.data.e_string.*, logger.Loc.Empty).data.e_string); + } + + if (part.tail.cooked.len() > 0) { + prev_part.tail.cooked.push(Expr.init(E.String, part.tail.cooked, part.tail_loc).data.e_string); + } + } else { + parts.appendAssumeCapacity(part); + } + } + } else { + parts.appendAssumeCapacity(part); + } + } + + if (parts.items.len == 0) { + parts.deinit(); + head.data.e_string.resolveRopeIfNeeded(allocator); + return head; + } + + return Expr.init(E.Template, .{ + .tag = null, + .parts = parts.items, + .head = .{ .cooked = head.data.e_string.* }, + }, loc); + } +}; + +pub const RegExp = struct { + value: string, + + // This exists for JavaScript bindings + // The RegExp constructor expects flags as a second argument. + // We want to avoid re-lexing the flags, so we store them here. + // This is the index of the first character in a flag, not the "/" + // /foo/gim + // ^ + flags_offset: ?u16 = null, + + pub var empty = RegExp{ .value = "" }; + + pub fn pattern(this: RegExp) string { + + // rewind until we reach the /foo/gim + // ^ + // should only ever be a single character + // but we're being cautious + if (this.flags_offset) |i_| { + var i = i_; + while (i > 0 and this.value[i] != '/') { + i -= 1; + } + + return std.mem.trim(u8, this.value[0..i], "/"); + } + + return std.mem.trim(u8, this.value, "/"); + } + + pub fn flags(this: RegExp) string { + // rewind until we reach the /foo/gim + // ^ + // should only ever be a single character + // but we're being cautious + if (this.flags_offset) |i| { + return this.value[i..]; + } + + return ""; + } + + pub fn jsonStringify(self: *const RegExp, writer: anytype) !void { + return try writer.write(self.value); + } +}; + +pub const Await = struct { + value: ExprNodeIndex, +}; + +pub const Yield = struct { + value: ?ExprNodeIndex = null, + is_star: bool = false, +}; + +pub const If = struct { + test_: ExprNodeIndex, + yes: ExprNodeIndex, + no: ExprNodeIndex, +}; + +pub const RequireString = struct { + import_record_index: u32 = 0, + + unwrapped_id: u32 = std.math.maxInt(u32), +}; + +pub const RequireResolveString = struct { + import_record_index: u32, + + // close_paren_loc: logger.Loc = logger.Loc.Empty, +}; + +pub const InlinedEnum = struct { + value: ExprNodeIndex, + comment: string, +}; + +pub const Import = struct { + expr: ExprNodeIndex, + options: ExprNodeIndex = Expr.empty, + import_record_index: u32, + + /// TODO: + /// Comments inside "import()" expressions have special meaning for Webpack. + /// Preserving comments inside these expressions makes it possible to use + /// esbuild as a TypeScript-to-JavaScript frontend for Webpack to improve + /// performance. We intentionally do not interpret these comments in esbuild + /// because esbuild is not Webpack. But we do preserve them since doing so is + /// harmless, easy to maintain, and useful to people. See the Webpack docs for + /// more info: https://webpack.js.org/api/module-methods/#magic-comments. + // leading_interior_comments: []G.Comment = &([_]G.Comment{}), + + pub fn isImportRecordNull(this: *const Import) bool { + return this.import_record_index == std.math.maxInt(u32); + } + + pub fn importRecordLoader(import: *const Import) ?bun.options.Loader { + // This logic is duplicated in js_printer.zig fn parsePath() + const obj = import.options.data.as(.e_object) orelse + return null; + const with = obj.get("with") orelse obj.get("assert") orelse + return null; + const with_obj = with.data.as(.e_object) orelse + return null; + const str = (with_obj.get("type") orelse + return null).data.as(.e_string) orelse + return null; + + if (!str.is_utf16) if (bun.options.Loader.fromString(str.data)) |loader| { + if (loader == .sqlite) { + const embed = with_obj.get("embed") orelse return loader; + const embed_str = embed.data.as(.e_string) orelse return loader; + if (embed_str.eqlComptime("true")) { + return .sqlite_embedded; + } + } + return loader; + }; + + return null; + } +}; + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const ComptimeStringMap = bun.ComptimeStringMap; +const Environment = bun.Environment; +const ImportRecord = bun.ImportRecord; +const JSC = bun.JSC; +const OOM = bun.OOM; +const logger = bun.logger; +const string = bun.string; +const stringZ = bun.stringZ; +const strings = bun.strings; +const Loader = bun.options.Loader; + +const js_ast = bun.js_ast; +const E = js_ast.E; +const Expr = js_ast.Expr; +const ExprNodeIndex = js_ast.ExprNodeIndex; +const ExprNodeList = js_ast.ExprNodeList; +const Flags = js_ast.Flags; +const Op = js_ast.Op; +const OptionalChain = js_ast.OptionalChain; +const Ref = js_ast.Ref; +const ToJSError = js_ast.ToJSError; + +const G = js_ast.G; +pub const Class = G.Class; diff --git a/src/ast/Expr.zig b/src/ast/Expr.zig new file mode 100644 index 0000000000..d3939cd938 --- /dev/null +++ b/src/ast/Expr.zig @@ -0,0 +1,3231 @@ +loc: logger.Loc, +data: Data, + +pub const empty = Expr{ .data = .{ .e_missing = E.Missing{} }, .loc = logger.Loc.Empty }; + +pub fn isAnonymousNamed(expr: Expr) bool { + return switch (expr.data) { + .e_arrow => true, + .e_function => |func| func.func.name == null, + .e_class => |class| class.class_name == null, + else => false, + }; +} + +pub fn clone(this: Expr, allocator: std.mem.Allocator) !Expr { + return .{ + .loc = this.loc, + .data = try this.data.clone(allocator), + }; +} + +pub fn deepClone(this: Expr, allocator: std.mem.Allocator) anyerror!Expr { + return .{ + .loc = this.loc, + .data = try this.data.deepClone(allocator), + }; +} + +pub fn wrapInArrow(this: Expr, allocator: std.mem.Allocator) !Expr { + var stmts = try allocator.alloc(Stmt, 1); + stmts[0] = Stmt.alloc(S.Return, S.Return{ .value = this }, this.loc); + + return Expr.init(E.Arrow, E.Arrow{ + .args = &.{}, + .body = .{ + .loc = this.loc, + .stmts = stmts, + }, + }, this.loc); +} + +pub fn canBeInlinedFromPropertyAccess(this: Expr) bool { + return switch (this.data) { + // if the array has a spread we must keep it + // https://github.com/oven-sh/bun/issues/2594 + .e_spread => false, + + .e_missing => false, + else => true, + }; +} + +pub fn canBeConstValue(this: Expr) bool { + return this.data.canBeConstValue(); +} + +pub fn canBeMoved(expr: Expr) bool { + return expr.data.canBeMoved(); +} + +pub fn unwrapInlined(expr: Expr) Expr { + if (expr.data.as(.e_inlined_enum)) |inlined| return inlined.value; + return expr; +} + +pub fn fromBlob( + blob: *const JSC.WebCore.Blob, + allocator: std.mem.Allocator, + mime_type_: ?MimeType, + log: *logger.Log, + loc: logger.Loc, +) !Expr { + const bytes = blob.sharedView(); + + const mime_type = mime_type_ orelse MimeType.init(blob.content_type, null, null); + + if (mime_type.category == .json) { + const source = &logger.Source.initPathString("fetch.json", bytes); + var out_expr = JSONParser.parseForMacro(source, log, allocator) catch { + return error.MacroFailed; + }; + out_expr.loc = loc; + + 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()) { + var output = MutableString.initEmpty(allocator); + output = try JSPrinter.quoteForJSON(bytes, output, true); + var list = output.toOwnedSlice(); + // remove the quotes + if (list.len > 0) { + list = list[1 .. list.len - 1]; + } + return Expr.init(E.String, E.String.init(list), loc); + } + + return Expr.init( + E.String, + E.String{ + .data = try JSC.ZigString.init(bytes).toBase64DataURL(allocator), + }, + loc, + ); +} + +pub inline fn initIdentifier(ref: Ref, loc: logger.Loc) Expr { + return Expr{ + .loc = loc, + .data = .{ + .e_identifier = E.Identifier.init(ref), + }, + }; +} + +pub fn toEmpty(expr: Expr) Expr { + return Expr{ .data = .{ .e_missing = E.Missing{} }, .loc = expr.loc }; +} +pub fn isEmpty(expr: Expr) bool { + return expr.data == .e_missing; +} +pub const Query = struct { expr: Expr, loc: logger.Loc, i: u32 = 0 }; + +pub fn hasAnyPropertyNamed(expr: *const Expr, comptime names: []const string) bool { + if (std.meta.activeTag(expr.data) != .e_object) return false; + const obj = expr.data.e_object; + if (obj.properties.len == 0) return false; + + for (obj.properties.slice()) |prop| { + if (prop.value == null) continue; + const key = prop.key orelse continue; + if (std.meta.activeTag(key.data) != .e_string) continue; + const key_str = key.data.e_string; + if (strings.eqlAnyComptime(key_str.data, names)) return true; + } + + return false; +} + +pub fn toJS(this: Expr, allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) ToJSError!JSC.JSValue { + return this.data.toJS(allocator, globalObject); +} + +pub inline fn isArray(this: *const Expr) bool { + return this.data == .e_array; +} + +pub inline fn isObject(this: *const Expr) bool { + return this.data == .e_object; +} + +pub fn get(expr: *const Expr, name: string) ?Expr { + return if (asProperty(expr, name)) |query| query.expr else null; +} + +/// Only use this for pretty-printing JSON. Do not use in transpiler. +/// +/// This does not handle edgecases like `-1` or stringifying arbitrary property lookups. +pub fn getByIndex(expr: *const Expr, index: u32, index_str: string, allocator: std.mem.Allocator) ?Expr { + switch (expr.data) { + .e_array => |array| { + if (index >= array.items.len) return null; + return array.items.slice()[index]; + }, + .e_object => |object| { + for (object.properties.sliceConst()) |*prop| { + const key = &(prop.key orelse continue); + switch (key.data) { + .e_string => |str| { + if (str.eql(string, index_str)) { + return prop.value; + } + }, + .e_number => |num| { + if (num.toU32() == index) { + return prop.value; + } + }, + else => {}, + } + } + + return null; + }, + .e_string => |str| { + if (str.len() > index) { + var slice = str.slice(allocator); + // TODO: this is not correct since .length refers to UTF-16 code units and not UTF-8 bytes + // However, since this is only used in the JSON prettifier for `bun pm view`, it's not a blocker for shipping. + if (slice.len > index) { + return Expr.init(E.String, .{ .data = slice[index..][0..1] }, expr.loc); + } + } + }, + else => {}, + } + + return null; +} + +/// This supports lookups like: +/// - `foo` +/// - `foo.bar` +/// - `foo[123]` +/// - `foo[123].bar` +/// - `foo[123].bar[456]` +/// - `foo[123].bar[456].baz` +/// - `foo[123].bar[456].baz.qux` // etc. +/// +/// This is not intended for use by the transpiler, instead by pretty printing JSON. +pub fn getPathMayBeIndex(expr: *const Expr, name: string) ?Expr { + if (name.len == 0) { + return null; + } + + if (strings.indexOfAny(name, "[.")) |idx| { + switch (name[idx]) { + '[' => { + const end_idx = strings.indexOfChar(name, ']') orelse return null; + var base_expr = expr; + if (idx > 0) { + const key = name[0..idx]; + base_expr = &(base_expr.get(key) orelse return null); + } + + const index_str = name[idx + 1 .. end_idx]; + const index = std.fmt.parseInt(u32, index_str, 10) catch return null; + const rest = if (name.len > end_idx) name[end_idx + 1 ..] else ""; + const result = &(base_expr.getByIndex(index, index_str, bun.default_allocator) orelse return null); + if (rest.len > 0) return result.getPathMayBeIndex(rest); + return result.*; + }, + '.' => { + const key = name[0..idx]; + const sub_expr = &(expr.get(key) orelse return null); + const subpath = if (name.len > idx) name[idx + 1 ..] else ""; + if (subpath.len > 0) { + return sub_expr.getPathMayBeIndex(subpath); + } + + return sub_expr.*; + }, + else => unreachable, + } + } + + return expr.get(name); +} + +/// Don't use this if you care about performance. +/// +/// Sets the value of a property, creating it if it doesn't exist. +/// `expr` must be an object. +pub fn set(expr: *Expr, allocator: std.mem.Allocator, name: string, value: Expr) OOM!void { + bun.assertWithLocation(expr.isObject(), @src()); + for (0..expr.data.e_object.properties.len) |i| { + const prop = &expr.data.e_object.properties.ptr[i]; + const key = prop.key orelse continue; + if (std.meta.activeTag(key.data) != .e_string) continue; + if (key.data.e_string.eql(string, name)) { + prop.value = value; + return; + } + } + + var new_props = expr.data.e_object.properties.listManaged(allocator); + try new_props.append(.{ + .key = Expr.init(E.String, .{ .data = name }, logger.Loc.Empty), + .value = value, + }); + + expr.data.e_object.properties = BabyList(G.Property).fromList(new_props); +} + +/// Don't use this if you care about performance. +/// +/// Sets the value of a property to a string, creating it if it doesn't exist. +/// `expr` must be an object. +pub fn setString(expr: *Expr, allocator: std.mem.Allocator, name: string, value: string) OOM!void { + bun.assertWithLocation(expr.isObject(), @src()); + for (0..expr.data.e_object.properties.len) |i| { + const prop = &expr.data.e_object.properties.ptr[i]; + const key = prop.key orelse continue; + if (std.meta.activeTag(key.data) != .e_string) continue; + if (key.data.e_string.eql(string, name)) { + prop.value = Expr.init(E.String, .{ .data = value }, logger.Loc.Empty); + return; + } + } + + var new_props = expr.data.e_object.properties.listManaged(allocator); + try new_props.append(.{ + .key = Expr.init(E.String, .{ .data = name }, logger.Loc.Empty), + .value = Expr.init(E.String, .{ .data = value }, logger.Loc.Empty), + }); + + expr.data.e_object.properties = BabyList(G.Property).fromList(new_props); +} + +pub fn getObject(expr: *const Expr, name: string) ?Expr { + if (expr.asProperty(name)) |query| { + if (query.expr.isObject()) { + return query.expr; + } + } + return null; +} + +pub fn getString(expr: *const Expr, allocator: std.mem.Allocator, name: string) OOM!?struct { string, logger.Loc } { + if (asProperty(expr, name)) |q| { + if (q.expr.asString(allocator)) |str| { + return .{ + str, + q.expr.loc, + }; + } + } + return null; +} + +pub fn getNumber(expr: *const Expr, name: string) ?struct { f64, logger.Loc } { + if (asProperty(expr, name)) |q| { + if (q.expr.asNumber()) |num| { + return .{ + num, + q.expr.loc, + }; + } + } + return null; +} + +pub fn getStringCloned(expr: *const Expr, allocator: std.mem.Allocator, name: string) OOM!?string { + return if (asProperty(expr, name)) |q| q.expr.asStringCloned(allocator) else null; +} + +pub fn getStringClonedZ(expr: *const Expr, allocator: std.mem.Allocator, name: string) OOM!?stringZ { + return if (asProperty(expr, name)) |q| q.expr.asStringZ(allocator) else null; +} + +pub fn getArray(expr: *const Expr, name: string) ?ArrayIterator { + return if (asProperty(expr, name)) |q| q.expr.asArray() else null; +} + +pub fn getRope(self: *const Expr, rope: *const E.Object.Rope) ?E.Object.RopeQuery { + if (self.get(rope.head.data.e_string.data)) |existing| { + switch (existing.data) { + .e_array => |array| { + if (rope.next) |next| { + if (array.items.last()) |end| { + return end.getRope(next); + } + } + + return E.Object.RopeQuery{ + .expr = existing, + .rope = rope, + }; + }, + .e_object => { + if (rope.next) |next| { + if (existing.getRope(next)) |end| { + return end; + } + } + + return E.Object.RopeQuery{ + .expr = existing, + .rope = rope, + }; + }, + else => return E.Object.RopeQuery{ + .expr = existing, + .rope = rope, + }, + } + } + + return null; +} + +// Making this comptime bloats the binary and doesn't seem to impact runtime performance. +pub fn asProperty(expr: *const Expr, name: string) ?Query { + if (std.meta.activeTag(expr.data) != .e_object) return null; + const obj = expr.data.e_object; + if (obj.properties.len == 0) return null; + + return obj.asProperty(name); +} + +pub fn asPropertyStringMap(expr: *const Expr, name: string, allocator: std.mem.Allocator) ?*bun.StringArrayHashMap(string) { + if (std.meta.activeTag(expr.data) != .e_object) return null; + const obj_ = expr.data.e_object; + if (obj_.properties.len == 0) return null; + const query = obj_.asProperty(name) orelse return null; + if (query.expr.data != .e_object) return null; + + const obj = query.expr.data.e_object; + var count: usize = 0; + for (obj.properties.slice()) |prop| { + const key = prop.key.?.asString(allocator) orelse continue; + const value = prop.value.?.asString(allocator) orelse continue; + count += @as(usize, @intFromBool(key.len > 0 and value.len > 0)); + } + + if (count == 0) return null; + var map = bun.StringArrayHashMap(string).init(allocator); + map.ensureUnusedCapacity(count) catch return null; + + for (obj.properties.slice()) |prop| { + const key = prop.key.?.asString(allocator) orelse continue; + const value = prop.value.?.asString(allocator) orelse continue; + + if (!(key.len > 0 and value.len > 0)) continue; + + map.putAssumeCapacity(key, value); + } + + const ptr = allocator.create(bun.StringArrayHashMap(string)) catch unreachable; + ptr.* = map; + return ptr; +} + +pub const ArrayIterator = struct { + array: *const E.Array, + index: u32, + + pub fn next(this: *ArrayIterator) ?Expr { + if (this.index >= this.array.items.len) { + return null; + } + defer this.index += 1; + return this.array.items.ptr[this.index]; + } +}; + +pub fn asArray(expr: *const Expr) ?ArrayIterator { + if (std.meta.activeTag(expr.data) != .e_array) return null; + const array = expr.data.e_array; + if (array.items.len == 0) return null; + + return ArrayIterator{ .array = array, .index = 0 }; +} + +pub inline fn asUtf8StringLiteral(expr: *const Expr) ?string { + if (expr.data == .e_string) { + bun.debugAssert(expr.data.e_string.next == null); + return expr.data.e_string.data; + } + return null; +} + +pub inline fn asStringLiteral(expr: *const Expr, allocator: std.mem.Allocator) ?string { + if (std.meta.activeTag(expr.data) != .e_string) return null; + return expr.data.e_string.string(allocator) catch null; +} + +pub inline fn isString(expr: *const Expr) bool { + return switch (expr.data) { + .e_string => true, + else => false, + }; +} + +pub inline fn asString(expr: *const Expr, allocator: std.mem.Allocator) ?string { + switch (expr.data) { + .e_string => |str| return str.string(allocator) catch bun.outOfMemory(), + else => return null, + } +} +pub inline fn asStringHash(expr: *const Expr, allocator: std.mem.Allocator, comptime hash_fn: *const fn (buf: []const u8) callconv(.Inline) u64) OOM!?u64 { + switch (expr.data) { + .e_string => |str| { + if (str.isUTF8()) return hash_fn(str.data); + const utf8_str = try str.string(allocator); + defer allocator.free(utf8_str); + return hash_fn(utf8_str); + }, + else => return null, + } +} + +pub inline fn asStringCloned(expr: *const Expr, allocator: std.mem.Allocator) OOM!?string { + switch (expr.data) { + .e_string => |str| return try str.stringCloned(allocator), + else => return null, + } +} + +pub inline fn asStringZ(expr: *const Expr, allocator: std.mem.Allocator) OOM!?stringZ { + switch (expr.data) { + .e_string => |str| return try str.stringZ(allocator), + else => return null, + } +} + +pub fn asBool( + expr: *const Expr, +) ?bool { + if (std.meta.activeTag(expr.data) != .e_boolean) return null; + + return expr.data.e_boolean.value; +} + +pub fn asNumber(expr: *const Expr) ?f64 { + if (expr.data != .e_number) return null; + + return expr.data.e_number.value; +} + +pub const EFlags = enum { none, ts_decorator }; + +const Serializable = struct { + type: Tag, + object: string, + value: Data, + loc: logger.Loc, +}; + +pub fn isMissing(a: *const Expr) bool { + return std.meta.activeTag(a.data) == Expr.Tag.e_missing; +} + +// The goal of this function is to "rotate" the AST if it's possible to use the +// left-associative property of the operator to avoid unnecessary parentheses. +// +// When using this, make absolutely sure that the operator is actually +// associative. For example, the "-" operator is not associative for +// floating-point numbers. +pub fn joinWithLeftAssociativeOp( + comptime op: Op.Code, + a: Expr, + b: Expr, + allocator: std.mem.Allocator, +) Expr { + // "(a, b) op c" => "a, b op c" + switch (a.data) { + .e_binary => |comma| { + if (comma.op == .bin_comma) { + comma.right = joinWithLeftAssociativeOp(op, comma.right, b, allocator); + } + }, + else => {}, + } + + // "a op (b op c)" => "(a op b) op c" + // "a op (b op (c op d))" => "((a op b) op c) op d" + switch (b.data) { + .e_binary => |binary| { + if (binary.op == op) { + return joinWithLeftAssociativeOp( + op, + joinWithLeftAssociativeOp(op, a, binary.left, allocator), + binary.right, + allocator, + ); + } + }, + else => {}, + } + + // "a op b" => "a op b" + // "(a op b) op c" => "(a op b) op c" + return Expr.init(E.Binary, E.Binary{ .op = op, .left = a, .right = b }, a.loc); +} + +pub fn joinWithComma(a: Expr, b: Expr, _: std.mem.Allocator) Expr { + if (a.isMissing()) { + return b; + } + + if (b.isMissing()) { + return a; + } + + return Expr.init(E.Binary, E.Binary{ .op = .bin_comma, .left = a, .right = b }, a.loc); +} + +pub fn joinAllWithComma(all: []Expr, allocator: std.mem.Allocator) Expr { + bun.assert(all.len > 0); + switch (all.len) { + 1 => { + return all[0]; + }, + 2 => { + return Expr.joinWithComma(all[0], all[1], allocator); + }, + else => { + var expr = all[0]; + for (1..all.len) |i| { + expr = Expr.joinWithComma(expr, all[i], allocator); + } + return expr; + }, + } +} + +pub fn joinAllWithCommaCallback(all: []Expr, comptime Context: type, ctx: Context, comptime callback: (fn (ctx: anytype, expr: Expr) ?Expr), allocator: std.mem.Allocator) ?Expr { + switch (all.len) { + 0 => return null, + 1 => { + return callback(ctx, all[0]); + }, + 2 => { + return Expr.joinWithComma( + callback(ctx, all[0]) orelse Expr{ + .data = .{ .e_missing = .{} }, + .loc = all[0].loc, + }, + callback(ctx, all[1]) orelse Expr{ + .data = .{ .e_missing = .{} }, + .loc = all[1].loc, + }, + allocator, + ); + }, + else => { + var i: usize = 1; + var expr = callback(ctx, all[0]) orelse Expr{ + .data = .{ .e_missing = .{} }, + .loc = all[0].loc, + }; + + while (i < all.len) : (i += 1) { + expr = Expr.joinWithComma(expr, callback(ctx, all[i]) orelse Expr{ + .data = .{ .e_missing = .{} }, + .loc = all[i].loc, + }, allocator); + } + + return expr; + }, + } +} + +pub fn jsonStringify(self: *const @This(), writer: anytype) !void { + return try writer.write(Serializable{ .type = std.meta.activeTag(self.data), .object = "expr", .value = self.data, .loc = self.loc }); +} + +pub fn extractNumericValues(left: Expr.Data, right: Expr.Data) ?[2]f64 { + return .{ + left.extractNumericValue() orelse return null, + right.extractNumericValue() orelse return null, + }; +} + +pub var icount: usize = 0; + +// We don't need to dynamically allocate booleans +var true_bool = E.Boolean{ .value = true }; +var false_bool = E.Boolean{ .value = false }; +var bool_values = [_]*E.Boolean{ &false_bool, &true_bool }; + +/// When the lifetime of an Expr.Data's pointer must exist longer than reset() is called, use this function. +/// Be careful to free the memory (or use an allocator that does it for you) +/// Also, prefer Expr.init or Expr.alloc when possible. This will be slower. +pub fn allocate(allocator: std.mem.Allocator, comptime Type: type, st: Type, loc: logger.Loc) Expr { + icount += 1; + Data.Store.assert(); + + switch (Type) { + E.Array => { + return Expr{ + .loc = loc, + .data = Data{ + .e_array = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.Class => { + return Expr{ + .loc = loc, + .data = Data{ + .e_class = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.Unary => { + return Expr{ + .loc = loc, + .data = Data{ + .e_unary = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.Binary => { + return Expr{ + .loc = loc, + .data = Data{ + .e_binary = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.This => { + return Expr{ + .loc = loc, + .data = Data{ + .e_this = st, + }, + }; + }, + E.Boolean => { + return Expr{ + .loc = loc, + .data = Data{ + .e_boolean = st, + }, + }; + }, + E.Super => { + return Expr{ + .loc = loc, + .data = Data{ + .e_super = st, + }, + }; + }, + E.Null => { + return Expr{ + .loc = loc, + .data = Data{ + .e_null = st, + }, + }; + }, + E.Undefined => { + return Expr{ + .loc = loc, + .data = Data{ + .e_undefined = st, + }, + }; + }, + E.New => { + return Expr{ + .loc = loc, + .data = Data{ + .e_new = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.NewTarget => { + return Expr{ + .loc = loc, + .data = Data{ + .e_new_target = st, + }, + }; + }, + E.Function => { + return Expr{ + .loc = loc, + .data = Data{ + .e_function = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.ImportMeta => { + return Expr{ + .loc = loc, + .data = Data{ + .e_import_meta = st, + }, + }; + }, + E.Call => { + return Expr{ + .loc = loc, + .data = Data{ + .e_call = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.Dot => { + return Expr{ + .loc = loc, + .data = Data{ + .e_dot = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.Index => { + return Expr{ + .loc = loc, + .data = Data{ + .e_index = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.Arrow => { + return Expr{ + .loc = loc, + .data = Data{ + .e_arrow = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.Identifier => { + return Expr{ + .loc = loc, + .data = Data{ + .e_identifier = E.Identifier{ + .ref = st.ref, + .must_keep_due_to_with_stmt = st.must_keep_due_to_with_stmt, + .can_be_removed_if_unused = st.can_be_removed_if_unused, + .call_can_be_unwrapped_if_unused = st.call_can_be_unwrapped_if_unused, + }, + }, + }; + }, + E.ImportIdentifier => { + return Expr{ + .loc = loc, + .data = Data{ + .e_import_identifier = .{ + .ref = st.ref, + .was_originally_identifier = st.was_originally_identifier, + }, + }, + }; + }, + E.CommonJSExportIdentifier => { + return Expr{ + .loc = loc, + .data = Data{ + .e_commonjs_export_identifier = .{ + .ref = st.ref, + }, + }, + }; + }, + + E.PrivateIdentifier => { + return Expr{ + .loc = loc, + .data = Data{ + .e_private_identifier = st, + }, + }; + }, + E.JSXElement => { + return Expr{ + .loc = loc, + .data = Data{ + .e_jsx_element = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.Missing => { + return Expr{ .loc = loc, .data = Data{ .e_missing = E.Missing{} } }; + }, + E.Number => { + return Expr{ + .loc = loc, + .data = Data{ + .e_number = st, + }, + }; + }, + E.BigInt => { + return Expr{ + .loc = loc, + .data = Data{ + .e_big_int = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.Object => { + return Expr{ + .loc = loc, + .data = Data{ + .e_object = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.Spread => { + return Expr{ + .loc = loc, + .data = Data{ + .e_spread = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.String => { + if (comptime Environment.isDebug) { + // Sanity check: assert string is not a null ptr + if (st.data.len > 0 and st.isUTF8()) { + bun.assert(@intFromPtr(st.data.ptr) > 0); + } + } + return Expr{ + .loc = loc, + .data = Data{ + .e_string = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + + E.Template => { + return Expr{ + .loc = loc, + .data = Data{ + .e_template = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.RegExp => { + return Expr{ + .loc = loc, + .data = Data{ + .e_reg_exp = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.Await => { + return Expr{ + .loc = loc, + .data = Data{ + .e_await = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.Yield => { + return Expr{ + .loc = loc, + .data = Data{ + .e_yield = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.If => { + return Expr{ + .loc = loc, + .data = Data{ + .e_if = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.RequireResolveString => { + return Expr{ + .loc = loc, + .data = Data{ + .e_require_resolve_string = st, + }, + }; + }, + E.Import => { + return Expr{ + .loc = loc, + .data = Data{ + .e_import = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st; + break :brk item; + }, + }, + }; + }, + E.RequireString => { + return Expr{ + .loc = loc, + .data = Data{ + .e_require_string = st, + }, + }; + }, + *E.String => { + return Expr{ + .loc = loc, + .data = Data{ + .e_string = brk: { + const item = allocator.create(Type) catch unreachable; + item.* = st.*; + break :brk item; + }, + }, + }; + }, + + else => { + @compileError("Invalid type passed to Expr.init: " ++ @typeName(Type)); + }, + } +} + +pub const Disabler = bun.DebugOnlyDisabler(@This()); + +pub fn init(comptime Type: type, st: Type, loc: logger.Loc) Expr { + icount += 1; + Data.Store.assert(); + + switch (Type) { + E.NameOfSymbol => { + return Expr{ + .loc = loc, + .data = Data{ + .e_name_of_symbol = Data.Store.append(E.NameOfSymbol, st), + }, + }; + }, + E.Array => { + return Expr{ + .loc = loc, + .data = Data{ + .e_array = Data.Store.append(Type, st), + }, + }; + }, + E.Class => { + return Expr{ + .loc = loc, + .data = Data{ + .e_class = Data.Store.append(Type, st), + }, + }; + }, + E.Unary => { + return Expr{ + .loc = loc, + .data = Data{ + .e_unary = Data.Store.append(Type, st), + }, + }; + }, + E.Binary => { + return Expr{ + .loc = loc, + .data = Data{ + .e_binary = Data.Store.append(Type, st), + }, + }; + }, + E.This => { + return Expr{ + .loc = loc, + .data = Data{ + .e_this = st, + }, + }; + }, + E.Boolean => { + return Expr{ + .loc = loc, + .data = Data{ + .e_boolean = st, + }, + }; + }, + E.Super => { + return Expr{ + .loc = loc, + .data = Data{ + .e_super = st, + }, + }; + }, + E.Null => { + return Expr{ + .loc = loc, + .data = Data{ + .e_null = st, + }, + }; + }, + E.Undefined => { + return Expr{ + .loc = loc, + .data = Data{ + .e_undefined = st, + }, + }; + }, + E.New => { + return Expr{ + .loc = loc, + .data = Data{ + .e_new = Data.Store.append(Type, st), + }, + }; + }, + E.NewTarget => { + return Expr{ + .loc = loc, + .data = Data{ + .e_new_target = st, + }, + }; + }, + E.Function => { + return Expr{ + .loc = loc, + .data = Data{ + .e_function = Data.Store.append(Type, st), + }, + }; + }, + E.ImportMeta => { + return Expr{ + .loc = loc, + .data = Data{ + .e_import_meta = st, + }, + }; + }, + E.Call => { + return Expr{ + .loc = loc, + .data = Data{ + .e_call = Data.Store.append(Type, st), + }, + }; + }, + E.Dot => { + return Expr{ + .loc = loc, + .data = Data{ + .e_dot = Data.Store.append(Type, st), + }, + }; + }, + E.Index => { + return Expr{ + .loc = loc, + .data = Data{ + .e_index = Data.Store.append(Type, st), + }, + }; + }, + E.Arrow => { + return Expr{ + .loc = loc, + .data = Data{ + .e_arrow = Data.Store.append(Type, st), + }, + }; + }, + E.Identifier => { + return Expr{ + .loc = loc, + .data = Data{ + .e_identifier = E.Identifier{ + .ref = st.ref, + .must_keep_due_to_with_stmt = st.must_keep_due_to_with_stmt, + .can_be_removed_if_unused = st.can_be_removed_if_unused, + .call_can_be_unwrapped_if_unused = st.call_can_be_unwrapped_if_unused, + }, + }, + }; + }, + E.ImportIdentifier => { + return Expr{ + .loc = loc, + .data = Data{ + .e_import_identifier = .{ + .ref = st.ref, + .was_originally_identifier = st.was_originally_identifier, + }, + }, + }; + }, + E.CommonJSExportIdentifier => { + return Expr{ + .loc = loc, + .data = Data{ + .e_commonjs_export_identifier = .{ + .ref = st.ref, + .base = st.base, + }, + }, + }; + }, + E.PrivateIdentifier => { + return Expr{ + .loc = loc, + .data = Data{ + .e_private_identifier = st, + }, + }; + }, + E.JSXElement => { + return Expr{ + .loc = loc, + .data = Data{ + .e_jsx_element = Data.Store.append(Type, st), + }, + }; + }, + E.Missing => { + return Expr{ .loc = loc, .data = Data{ .e_missing = E.Missing{} } }; + }, + E.Number => { + return Expr{ + .loc = loc, + .data = Data{ + .e_number = st, + }, + }; + }, + E.BigInt => { + return Expr{ + .loc = loc, + .data = Data{ + .e_big_int = Data.Store.append(Type, st), + }, + }; + }, + E.Object => { + return Expr{ + .loc = loc, + .data = Data{ + .e_object = Data.Store.append(Type, st), + }, + }; + }, + E.Spread => { + return Expr{ + .loc = loc, + .data = Data{ + .e_spread = Data.Store.append(Type, st), + }, + }; + }, + E.String => { + if (comptime Environment.isDebug) { + // Sanity check: assert string is not a null ptr + if (st.data.len > 0 and st.isUTF8()) { + bun.assert(@intFromPtr(st.data.ptr) > 0); + } + } + return Expr{ + .loc = loc, + .data = Data{ + .e_string = Data.Store.append(Type, st), + }, + }; + }, + + E.Template => { + return Expr{ + .loc = loc, + .data = Data{ + .e_template = Data.Store.append(Type, st), + }, + }; + }, + E.RegExp => { + return Expr{ + .loc = loc, + .data = Data{ + .e_reg_exp = Data.Store.append(Type, st), + }, + }; + }, + E.Await => { + return Expr{ + .loc = loc, + .data = Data{ + .e_await = Data.Store.append(Type, st), + }, + }; + }, + E.Yield => { + return Expr{ + .loc = loc, + .data = Data{ + .e_yield = Data.Store.append(Type, st), + }, + }; + }, + E.If => { + return Expr{ + .loc = loc, + .data = Data{ + .e_if = Data.Store.append(Type, st), + }, + }; + }, + E.RequireResolveString => { + return Expr{ + .loc = loc, + .data = Data{ + .e_require_resolve_string = st, + }, + }; + }, + E.Import => { + return Expr{ + .loc = loc, + .data = Data{ + .e_import = Data.Store.append(Type, st), + }, + }; + }, + E.RequireString => { + return Expr{ + .loc = loc, + .data = Data{ + .e_require_string = st, + }, + }; + }, + *E.String => { + return Expr{ + .loc = loc, + .data = Data{ + .e_string = Data.Store.append(@TypeOf(st.*), st.*), + }, + }; + }, + E.InlinedEnum => return .{ .loc = loc, .data = .{ + .e_inlined_enum = Data.Store.append(@TypeOf(st), st), + } }, + + else => { + @compileError("Invalid type passed to Expr.init: " ++ @typeName(Type)); + }, + } +} + +pub fn isPrimitiveLiteral(this: Expr) bool { + return @as(Tag, this.data).isPrimitiveLiteral(); +} + +pub fn isRef(this: Expr, ref: Ref) bool { + return switch (this.data) { + .e_import_identifier => |import_identifier| import_identifier.ref.eql(ref), + .e_identifier => |ident| ident.ref.eql(ref), + else => false, + }; +} + +pub const Tag = enum { + e_array, + e_unary, + e_binary, + e_class, + e_new, + e_function, + e_call, + e_dot, + e_index, + e_arrow, + e_jsx_element, + e_object, + e_spread, + e_template, + e_reg_exp, + e_await, + e_yield, + e_if, + e_import, + e_identifier, + e_import_identifier, + e_private_identifier, + e_commonjs_export_identifier, + e_boolean, + e_number, + e_big_int, + e_string, + e_require_string, + e_require_resolve_string, + e_require_call_target, + e_require_resolve_call_target, + e_missing, + e_this, + e_super, + e_null, + e_undefined, + e_new_target, + e_import_meta, + e_import_meta_main, + e_require_main, + e_special, + e_inlined_enum, + e_name_of_symbol, + + // object, regex and array may have had side effects + pub fn isPrimitiveLiteral(tag: Tag) bool { + return switch (tag) { + .e_null, .e_undefined, .e_string, .e_boolean, .e_number, .e_big_int => true, + else => false, + }; + } + + pub fn typeof(tag: Tag) ?string { + return switch (tag) { + .e_array, .e_object, .e_null, .e_reg_exp => "object", + .e_undefined => "undefined", + .e_boolean => "boolean", + .e_number => "number", + .e_big_int => "bigint", + .e_string => "string", + .e_class, .e_function, .e_arrow => "function", + else => null, + }; + } + + pub fn format(tag: Tag, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try switch (tag) { + .e_string => writer.writeAll("string"), + .e_array => writer.writeAll("array"), + .e_unary => writer.writeAll("unary"), + .e_binary => writer.writeAll("binary"), + .e_boolean => writer.writeAll("boolean"), + .e_super => writer.writeAll("super"), + .e_null => writer.writeAll("null"), + .e_undefined => writer.writeAll("undefined"), + .e_new => writer.writeAll("new"), + .e_function => writer.writeAll("function"), + .e_new_target => writer.writeAll("new target"), + .e_import_meta => writer.writeAll("import.meta"), + .e_call => writer.writeAll("call"), + .e_dot => writer.writeAll("dot"), + .e_index => writer.writeAll("index"), + .e_arrow => writer.writeAll("arrow"), + .e_identifier => writer.writeAll("identifier"), + .e_import_identifier => writer.writeAll("import identifier"), + .e_private_identifier => writer.writeAll("#privateIdentifier"), + .e_jsx_element => writer.writeAll(""), + .e_missing => writer.writeAll(""), + .e_number => writer.writeAll("number"), + .e_big_int => writer.writeAll("BigInt"), + .e_object => writer.writeAll("object"), + .e_spread => writer.writeAll("..."), + .e_template => writer.writeAll("template"), + .e_reg_exp => writer.writeAll("regexp"), + .e_await => writer.writeAll("await"), + .e_yield => writer.writeAll("yield"), + .e_if => writer.writeAll("if"), + .e_require_resolve_string => writer.writeAll("require_or_require_resolve"), + .e_import => writer.writeAll("import"), + .e_this => writer.writeAll("this"), + .e_class => writer.writeAll("class"), + .e_require_string => writer.writeAll("require"), + else => writer.writeAll(@tagName(tag)), + }; + } + + pub fn jsonStringify(self: @This(), writer: anytype) !void { + return try writer.write(@tagName(self)); + } + + pub fn isArray(self: Tag) bool { + switch (self) { + .e_array => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isUnary(self: Tag) bool { + switch (self) { + .e_unary => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isBinary(self: Tag) bool { + switch (self) { + .e_binary => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isThis(self: Tag) bool { + switch (self) { + .e_this => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isClass(self: Tag) bool { + switch (self) { + .e_class => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isBoolean(self: Tag) bool { + switch (self) { + .e_boolean => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isSuper(self: Tag) bool { + switch (self) { + .e_super => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isNull(self: Tag) bool { + switch (self) { + .e_null => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isUndefined(self: Tag) bool { + switch (self) { + .e_undefined => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isNew(self: Tag) bool { + switch (self) { + .e_new => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isNewTarget(self: Tag) bool { + switch (self) { + .e_new_target => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isFunction(self: Tag) bool { + switch (self) { + .e_function => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isImportMeta(self: Tag) bool { + switch (self) { + .e_import_meta => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isCall(self: Tag) bool { + switch (self) { + .e_call => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isDot(self: Tag) bool { + switch (self) { + .e_dot => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isIndex(self: Tag) bool { + switch (self) { + .e_index => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isArrow(self: Tag) bool { + switch (self) { + .e_arrow => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isIdentifier(self: Tag) bool { + switch (self) { + .e_identifier => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isImportIdentifier(self: Tag) bool { + switch (self) { + .e_import_identifier => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isPrivateIdentifier(self: Tag) bool { + switch (self) { + .e_private_identifier => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isJsxElement(self: Tag) bool { + switch (self) { + .e_jsx_element => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isMissing(self: Tag) bool { + switch (self) { + .e_missing => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isNumber(self: Tag) bool { + switch (self) { + .e_number => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isBigInt(self: Tag) bool { + switch (self) { + .e_big_int => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isObject(self: Tag) bool { + switch (self) { + .e_object => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isSpread(self: Tag) bool { + switch (self) { + .e_spread => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isString(self: Tag) bool { + switch (self) { + .e_string => { + return true; + }, + else => { + return false; + }, + } + } + + pub fn isTemplate(self: Tag) bool { + switch (self) { + .e_template => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isRegExp(self: Tag) bool { + switch (self) { + .e_reg_exp => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isAwait(self: Tag) bool { + switch (self) { + .e_await => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isYield(self: Tag) bool { + switch (self) { + .e_yield => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isIf(self: Tag) bool { + switch (self) { + .e_if => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isRequireResolveString(self: Tag) bool { + switch (self) { + .e_require_resolve_string => { + return true; + }, + else => { + return false; + }, + } + } + pub fn isImport(self: Tag) bool { + switch (self) { + .e_import => { + return true; + }, + else => { + return false; + }, + } + } +}; + +pub fn isBoolean(a: Expr) bool { + switch (a.data) { + .e_boolean => { + return true; + }, + + .e_if => |ex| { + return isBoolean(ex.yes) and isBoolean(ex.no); + }, + .e_unary => |ex| { + return ex.op == .un_not or ex.op == .un_delete; + }, + .e_binary => |ex| { + switch (ex.op) { + .bin_strict_eq, .bin_strict_ne, .bin_loose_eq, .bin_loose_ne, .bin_lt, .bin_gt, .bin_le, .bin_ge, .bin_instanceof, .bin_in => { + return true; + }, + .bin_logical_or => { + return isBoolean(ex.left) and isBoolean(ex.right); + }, + .bin_logical_and => { + return isBoolean(ex.left) and isBoolean(ex.right); + }, + else => {}, + } + }, + else => {}, + } + + return false; +} + +pub fn assign(a: Expr, b: Expr) Expr { + return init(E.Binary, E.Binary{ + .op = .bin_assign, + .left = a, + .right = b, + }, a.loc); +} +pub inline fn at(expr: Expr, comptime Type: type, t: Type, _: std.mem.Allocator) Expr { + return init(Type, t, expr.loc); +} + +// Wraps the provided expression in the "!" prefix operator. The expression +// will potentially be simplified to avoid generating unnecessary extra "!" +// operators. For example, calling this with "!!x" will return "!x" instead +// of returning "!!!x". +pub fn not(expr: Expr, allocator: std.mem.Allocator) Expr { + return maybeSimplifyNot( + expr, + allocator, + ) orelse Expr.init( + E.Unary, + E.Unary{ + .op = .un_not, + .value = expr, + }, + expr.loc, + ); +} + +pub fn hasValueForThisInCall(expr: Expr) bool { + return switch (expr.data) { + .e_dot, .e_index => true, + else => false, + }; +} + +/// 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 => { + return expr.at(E.Boolean, E.Boolean{ .value = true }, allocator); + }, + .e_boolean => |b| { + return expr.at(E.Boolean, E.Boolean{ .value = b.value }, allocator); + }, + .e_number => |n| { + return expr.at(E.Boolean, E.Boolean{ .value = (n.value == 0 or std.math.isNan(n.value)) }, allocator); + }, + .e_big_int => |b| { + return expr.at(E.Boolean, E.Boolean{ .value = strings.eqlComptime(b.value, "0") }, allocator); + }, + .e_function, + .e_arrow, + .e_reg_exp, + => { + return expr.at(E.Boolean, E.Boolean{ .value = false }, allocator); + }, + // "!!!a" => "!a" + .e_unary => |un| { + if (un.op == Op.Code.un_not and knownPrimitive(un.value) == .boolean) { + return un.value; + } + }, + .e_binary => |ex| { + // TODO: evaluate whether or not it is safe to do this mutation since it's modifying in-place. + // Make sure that these transformations are all safe for special values. + // For example, "!(a < b)" is not the same as "a >= b" if a and/or b are + // NaN (or undefined, or null, or possibly other problem cases too). + switch (ex.op) { + Op.Code.bin_loose_eq => { + // "!(a == b)" => "a != b" + ex.op = .bin_loose_ne; + return expr; + }, + Op.Code.bin_loose_ne => { + // "!(a != b)" => "a == b" + ex.op = .bin_loose_eq; + return expr; + }, + Op.Code.bin_strict_eq => { + // "!(a === b)" => "a !== b" + ex.op = .bin_strict_ne; + return expr; + }, + Op.Code.bin_strict_ne => { + // "!(a !== b)" => "a === b" + ex.op = .bin_strict_eq; + return expr; + }, + Op.Code.bin_comma => { + // "!(a, b)" => "a, !b" + ex.right = ex.right.not(allocator); + return expr; + }, + else => {}, + } + }, + .e_inlined_enum => |inlined| { + return maybeSimplifyNot(inlined.value, allocator); + }, + + else => {}, + } + + return null; +} + +pub fn toStringExprWithoutSideEffects(expr: Expr, allocator: std.mem.Allocator) ?Expr { + const unwrapped = expr.unwrapInlined(); + const slice = switch (unwrapped.data) { + .e_null => "null", + .e_string => return expr, + .e_undefined => "undefined", + .e_boolean => |data| if (data.value) "true" else "false", + .e_big_int => |bigint| bigint.value, + .e_number => |num| if (num.toString(allocator)) |str| + str + else + null, + .e_reg_exp => |regexp| regexp.value, + .e_dot => |dot| @as(?[]const u8, brk: { + // This is dumb but some JavaScript obfuscators use this to generate string literals + if (bun.strings.eqlComptime(dot.name, "constructor")) { + break :brk switch (dot.target.data) { + .e_string => "function String() { [native code] }", + .e_reg_exp => "function RegExp() { [native code] }", + else => null, + }; + } + break :brk null; + }), + else => null, + }; + return if (slice) |s| Expr.init(E.String, E.String.init(s), expr.loc) else null; +} + +pub fn isOptionalChain(self: *const @This()) bool { + return switch (self.data) { + .e_dot => self.data.e_dot.optional_chain != null, + .e_index => self.data.e_index.optional_chain != null, + .e_call => self.data.e_call.optional_chain != null, + else => false, + }; +} + +pub inline fn knownPrimitive(self: @This()) PrimitiveType { + return self.data.knownPrimitive(); +} + +pub const PrimitiveType = enum { + unknown, + mixed, + null, + undefined, + boolean, + number, + string, + bigint, + + pub const static = std.enums.EnumSet(PrimitiveType).init(.{ + .mixed = true, + .null = true, + .undefined = true, + .boolean = true, + .number = true, + .string = true, + // for our purposes, bigint is dynamic + // it is technically static though + // .@"bigint" = true, + }); + + pub inline fn isStatic(this: PrimitiveType) bool { + return static.contains(this); + } + + pub fn merge(left_known: PrimitiveType, right_known: PrimitiveType) PrimitiveType { + if (right_known == .unknown or left_known == .unknown) + return .unknown; + + return if (left_known == right_known) + left_known + else + .mixed; + } +}; + +pub const Data = union(Tag) { + e_array: *E.Array, + e_unary: *E.Unary, + e_binary: *E.Binary, + e_class: *E.Class, + + e_new: *E.New, + e_function: *E.Function, + e_call: *E.Call, + e_dot: *E.Dot, + e_index: *E.Index, + e_arrow: *E.Arrow, + + e_jsx_element: *E.JSXElement, + e_object: *E.Object, + e_spread: *E.Spread, + e_template: *E.Template, + e_reg_exp: *E.RegExp, + e_await: *E.Await, + e_yield: *E.Yield, + e_if: *E.If, + e_import: *E.Import, + + e_identifier: E.Identifier, + e_import_identifier: E.ImportIdentifier, + e_private_identifier: E.PrivateIdentifier, + e_commonjs_export_identifier: E.CommonJSExportIdentifier, + + e_boolean: E.Boolean, + e_number: E.Number, + e_big_int: *E.BigInt, + e_string: *E.String, + + e_require_string: E.RequireString, + e_require_resolve_string: E.RequireResolveString, + e_require_call_target, + e_require_resolve_call_target, + + e_missing: E.Missing, + e_this: E.This, + e_super: E.Super, + e_null: E.Null, + e_undefined: E.Undefined, + e_new_target: E.NewTarget, + e_import_meta: E.ImportMeta, + + e_import_meta_main: E.ImportMetaMain, + e_require_main, + + /// Covers some exotic AST node types under one namespace, since the + /// places this is found it all follows similar handling. + e_special: E.Special, + + e_inlined_enum: *E.InlinedEnum, + + e_name_of_symbol: *E.NameOfSymbol, + + comptime { + bun.assert_eql(@sizeOf(Data), 24); // Do not increase the size of Expr + } + + pub fn as(data: Data, comptime tag: Tag) ?@FieldType(Data, @tagName(tag)) { + return if (data == tag) @field(data, @tagName(tag)) else null; + } + + pub fn clone(this: Expr.Data, allocator: std.mem.Allocator) !Data { + return switch (this) { + .e_array => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_array))); + item.* = el.*; + return .{ .e_array = item }; + }, + .e_unary => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_unary))); + item.* = el.*; + return .{ .e_unary = item }; + }, + .e_binary => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_binary))); + item.* = el.*; + return .{ .e_binary = item }; + }, + .e_class => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_class))); + item.* = el.*; + return .{ .e_class = item }; + }, + .e_new => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_new))); + item.* = el.*; + return .{ .e_new = item }; + }, + .e_function => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_function))); + item.* = el.*; + return .{ .e_function = item }; + }, + .e_call => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_call))); + item.* = el.*; + return .{ .e_call = item }; + }, + .e_dot => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_dot))); + item.* = el.*; + return .{ .e_dot = item }; + }, + .e_index => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_index))); + item.* = el.*; + return .{ .e_index = item }; + }, + .e_arrow => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_arrow))); + item.* = el.*; + return .{ .e_arrow = item }; + }, + .e_jsx_element => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_jsx_element))); + item.* = el.*; + return .{ .e_jsx_element = item }; + }, + .e_object => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_object))); + item.* = el.*; + return .{ .e_object = item }; + }, + .e_spread => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_spread))); + item.* = el.*; + return .{ .e_spread = item }; + }, + .e_template => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_template))); + item.* = el.*; + return .{ .e_template = item }; + }, + .e_reg_exp => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_reg_exp))); + item.* = el.*; + return .{ .e_reg_exp = item }; + }, + .e_await => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_await))); + item.* = el.*; + return .{ .e_await = item }; + }, + .e_yield => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_yield))); + item.* = el.*; + return .{ .e_yield = item }; + }, + .e_if => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_if))); + item.* = el.*; + return .{ .e_if = item }; + }, + .e_import => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_import))); + item.* = el.*; + return .{ .e_import = item }; + }, + .e_big_int => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_big_int))); + item.* = el.*; + return .{ .e_big_int = item }; + }, + .e_string => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_string))); + item.* = el.*; + return .{ .e_string = item }; + }, + .e_inlined_enum => |el| { + const item = try allocator.create(std.meta.Child(@TypeOf(this.e_inlined_enum))); + item.* = el.*; + return .{ .e_inlined_enum = item }; + }, + else => this, + }; + } + + pub fn deepClone(this: Expr.Data, allocator: std.mem.Allocator) !Data { + return switch (this) { + .e_array => |el| { + const items = try el.items.deepClone(allocator); + const item = bun.create(allocator, E.Array, .{ + .items = items, + .comma_after_spread = el.comma_after_spread, + .was_originally_macro = el.was_originally_macro, + .is_single_line = el.is_single_line, + .is_parenthesized = el.is_parenthesized, + .close_bracket_loc = el.close_bracket_loc, + }); + return .{ .e_array = item }; + }, + .e_unary => |el| { + const item = bun.create(allocator, E.Unary, .{ + .op = el.op, + .value = try el.value.deepClone(allocator), + }); + return .{ .e_unary = item }; + }, + .e_binary => |el| { + const item = bun.create(allocator, E.Binary, .{ + .op = el.op, + .left = try el.left.deepClone(allocator), + .right = try el.right.deepClone(allocator), + }); + return .{ .e_binary = item }; + }, + .e_class => |el| { + const properties = try allocator.alloc(G.Property, el.properties.len); + for (el.properties, 0..) |prop, i| { + properties[i] = try prop.deepClone(allocator); + } + + const item = bun.create(allocator, E.Class, .{ + .class_keyword = el.class_keyword, + .ts_decorators = try el.ts_decorators.deepClone(allocator), + .class_name = el.class_name, + .extends = if (el.extends) |e| try e.deepClone(allocator) else null, + .body_loc = el.body_loc, + .close_brace_loc = el.close_brace_loc, + .properties = properties, + .has_decorators = el.has_decorators, + }); + return .{ .e_class = item }; + }, + .e_new => |el| { + const item = bun.create(allocator, E.New, .{ + .target = try el.target.deepClone(allocator), + .args = try el.args.deepClone(allocator), + .can_be_unwrapped_if_unused = el.can_be_unwrapped_if_unused, + .close_parens_loc = el.close_parens_loc, + }); + + return .{ .e_new = item }; + }, + .e_function => |el| { + const item = bun.create(allocator, E.Function, .{ + .func = try el.func.deepClone(allocator), + }); + return .{ .e_function = item }; + }, + .e_call => |el| { + const item = bun.create(allocator, E.Call, .{ + .target = try el.target.deepClone(allocator), + .args = try el.args.deepClone(allocator), + .optional_chain = el.optional_chain, + .is_direct_eval = el.is_direct_eval, + .close_paren_loc = el.close_paren_loc, + .can_be_unwrapped_if_unused = el.can_be_unwrapped_if_unused, + .was_jsx_element = el.was_jsx_element, + }); + return .{ .e_call = item }; + }, + .e_dot => |el| { + const item = bun.create(allocator, E.Dot, .{ + .target = try el.target.deepClone(allocator), + .name = el.name, + .name_loc = el.name_loc, + .optional_chain = el.optional_chain, + .can_be_removed_if_unused = el.can_be_removed_if_unused, + .call_can_be_unwrapped_if_unused = el.call_can_be_unwrapped_if_unused, + }); + return .{ .e_dot = item }; + }, + .e_index => |el| { + const item = bun.create(allocator, E.Index, .{ + .target = try el.target.deepClone(allocator), + .index = try el.index.deepClone(allocator), + .optional_chain = el.optional_chain, + }); + return .{ .e_index = item }; + }, + .e_arrow => |el| { + const args = try allocator.alloc(G.Arg, el.args.len); + for (0..args.len) |i| { + args[i] = try el.args[i].deepClone(allocator); + } + const item = bun.create(allocator, E.Arrow, .{ + .args = args, + .body = el.body, + .is_async = el.is_async, + .has_rest_arg = el.has_rest_arg, + .prefer_expr = el.prefer_expr, + }); + + return .{ .e_arrow = item }; + }, + .e_jsx_element => |el| { + const item = bun.create(allocator, E.JSXElement, .{ + .tag = if (el.tag) |tag| try tag.deepClone(allocator) else null, + .properties = try el.properties.deepClone(allocator), + .children = try el.children.deepClone(allocator), + .key_prop_index = el.key_prop_index, + .flags = el.flags, + .close_tag_loc = el.close_tag_loc, + }); + return .{ .e_jsx_element = item }; + }, + .e_object => |el| { + const item = bun.create(allocator, E.Object, .{ + .properties = try el.properties.deepClone(allocator), + .comma_after_spread = el.comma_after_spread, + .is_single_line = el.is_single_line, + .is_parenthesized = el.is_parenthesized, + .was_originally_macro = el.was_originally_macro, + .close_brace_loc = el.close_brace_loc, + }); + return .{ .e_object = item }; + }, + .e_spread => |el| { + const item = bun.create(allocator, E.Spread, .{ + .value = try el.value.deepClone(allocator), + }); + return .{ .e_spread = item }; + }, + .e_template => |el| { + const item = bun.create(allocator, E.Template, .{ + .tag = if (el.tag) |tag| try tag.deepClone(allocator) else null, + .parts = el.parts, + .head = el.head, + }); + return .{ .e_template = item }; + }, + .e_reg_exp => |el| { + const item = bun.create(allocator, E.RegExp, .{ + .value = el.value, + .flags_offset = el.flags_offset, + }); + return .{ .e_reg_exp = item }; + }, + .e_await => |el| { + const item = bun.create(allocator, E.Await, .{ + .value = try el.value.deepClone(allocator), + }); + return .{ .e_await = item }; + }, + .e_yield => |el| { + const item = bun.create(allocator, E.Yield, .{ + .value = if (el.value) |value| try value.deepClone(allocator) else null, + .is_star = el.is_star, + }); + return .{ .e_yield = item }; + }, + .e_if => |el| { + const item = bun.create(allocator, E.If, .{ + .test_ = try el.test_.deepClone(allocator), + .yes = try el.yes.deepClone(allocator), + .no = try el.no.deepClone(allocator), + }); + return .{ .e_if = item }; + }, + .e_import => |el| { + const item = bun.create(allocator, E.Import, .{ + .expr = try el.expr.deepClone(allocator), + .options = try el.options.deepClone(allocator), + .import_record_index = el.import_record_index, + }); + return .{ .e_import = item }; + }, + .e_big_int => |el| { + const item = bun.create(allocator, E.BigInt, .{ + .value = el.value, + }); + return .{ .e_big_int = item }; + }, + .e_string => |el| { + const item = bun.create(allocator, E.String, .{ + .data = el.data, + .prefer_template = el.prefer_template, + .next = el.next, + .end = el.end, + .rope_len = el.rope_len, + .is_utf16 = el.is_utf16, + }); + return .{ .e_string = item }; + }, + .e_inlined_enum => |el| { + const item = bun.create(allocator, E.InlinedEnum, .{ + .value = el.value, + .comment = el.comment, + }); + return .{ .e_inlined_enum = item }; + }, + else => this, + }; + } + + /// `hasher` should be something with 'pub fn update([]const u8) void'; + /// symbol table is passed to serialize `Ref` as an identifier names instead of a nondeterministic numbers + pub fn writeToHasher(this: Expr.Data, hasher: anytype, symbol_table: anytype) void { + writeAnyToHasher(hasher, std.meta.activeTag(this)); + switch (this) { + .e_name_of_symbol => |e| { + const symbol = e.ref.getSymbol(symbol_table); + hasher.update(symbol.original_name); + }, + .e_array => |e| { + writeAnyToHasher(hasher, .{ + e.is_single_line, + e.is_parenthesized, + e.was_originally_macro, + e.items.len, + }); + for (e.items.slice()) |item| { + item.data.writeToHasher(hasher, symbol_table); + } + }, + .e_unary => |e| { + writeAnyToHasher(hasher, .{e.op}); + e.value.data.writeToHasher(hasher, symbol_table); + }, + .e_binary => |e| { + writeAnyToHasher(hasher, .{e.op}); + e.left.data.writeToHasher(hasher, symbol_table); + e.right.data.writeToHasher(hasher, symbol_table); + }, + .e_class => |e| { + _ = e; // autofix + }, + inline .e_new, .e_call => |e| { + _ = e; // autofix + }, + .e_function => |e| { + _ = e; // autofix + }, + .e_dot => |e| { + writeAnyToHasher(hasher, .{ e.optional_chain, e.name.len }); + e.target.data.writeToHasher(hasher, symbol_table); + hasher.update(e.name); + }, + .e_index => |e| { + writeAnyToHasher(hasher, .{e.optional_chain}); + e.target.data.writeToHasher(hasher, symbol_table); + e.index.data.writeToHasher(hasher, symbol_table); + }, + .e_arrow => |e| { + _ = e; // autofix + }, + .e_jsx_element => |e| { + _ = e; // autofix + }, + .e_object => |e| { + _ = e; // autofix + }, + inline .e_spread, .e_await => |e| { + e.value.data.writeToHasher(hasher, symbol_table); + }, + inline .e_yield => |e| { + writeAnyToHasher(hasher, .{ e.is_star, e.value }); + if (e.value) |value| + value.data.writeToHasher(hasher, symbol_table); + }, + .e_template => |e| { + _ = e; // autofix + }, + .e_if => |e| { + _ = e; // autofix + }, + .e_import => |e| { + _ = e; // autofix + + }, + inline .e_identifier, + .e_import_identifier, + .e_private_identifier, + .e_commonjs_export_identifier, + => |e| { + const symbol = e.ref.getSymbol(symbol_table); + hasher.update(symbol.original_name); + }, + inline .e_boolean, .e_number => |e| { + writeAnyToHasher(hasher, e.value); + }, + inline .e_big_int, .e_reg_exp => |e| { + hasher.update(e.value); + }, + + .e_string => |e| { + var next: ?*E.String = e; + if (next) |current| { + if (current.isUTF8()) { + hasher.update(current.data); + } else { + hasher.update(bun.reinterpretSlice(u8, current.slice16())); + } + next = current.next; + hasher.update("\x00"); + } + }, + inline .e_require_string, .e_require_resolve_string => |e| { + writeAnyToHasher(hasher, e.import_record_index); // preferably, i'd like to write the filepath + }, + + .e_import_meta_main => |e| { + writeAnyToHasher(hasher, e.inverted); + }, + .e_inlined_enum => |e| { + // pretend there is no comment + e.value.data.writeToHasher(hasher, symbol_table); + }, + + // no data + .e_require_call_target, + .e_require_resolve_call_target, + .e_missing, + .e_this, + .e_super, + .e_null, + .e_undefined, + .e_new_target, + .e_require_main, + .e_import_meta, + .e_special, + => {}, + } + } + + /// "const values" here refers to expressions that can participate in constant + /// inlining, as they have no side effects on instantiation, and there would be + /// no observable difference if duplicated. This is a subset of canBeMoved() + pub fn canBeConstValue(this: Expr.Data) bool { + return switch (this) { + .e_number, + .e_boolean, + .e_null, + .e_undefined, + .e_inlined_enum, + => true, + .e_string => |str| str.next == null, + .e_array => |array| array.was_originally_macro, + .e_object => |object| object.was_originally_macro, + else => false, + }; + } + + /// Expressions that can be moved are those that do not have side + /// effects on their own. This is used to determine what can be moved + /// outside of a module wrapper (__esm/__commonJS). + pub fn canBeMoved(data: Expr.Data) bool { + return switch (data) { + // TODO: identifiers can be removed if unused, however code that + // moves expressions around sometimes does so incorrectly when + // doing destructures. test case: https://github.com/oven-sh/bun/issues/14027 + // .e_identifier => |id| id.can_be_removed_if_unused, + + .e_class => |class| class.canBeMoved(), + + .e_arrow, + .e_function, + + .e_number, + .e_boolean, + .e_null, + .e_undefined, + // .e_reg_exp, + .e_big_int, + .e_string, + .e_inlined_enum, + .e_import_meta, + => true, + + .e_template => |template| template.tag == null and template.parts.len == 0, + + .e_array => |array| array.was_originally_macro, + .e_object => |object| object.was_originally_macro, + + // TODO: experiment with allowing some e_binary, e_unary, e_if as movable + + else => false, + }; + } + + pub fn knownPrimitive(data: Expr.Data) PrimitiveType { + return switch (data) { + .e_big_int => .bigint, + .e_boolean => .boolean, + .e_null => .null, + .e_number => .number, + .e_string => .string, + .e_undefined => .undefined, + .e_template => if (data.e_template.tag == null) PrimitiveType.string else PrimitiveType.unknown, + .e_if => mergeKnownPrimitive(data.e_if.yes.data, data.e_if.no.data), + .e_binary => |binary| brk: { + switch (binary.op) { + .bin_strict_eq, + .bin_strict_ne, + .bin_loose_eq, + .bin_loose_ne, + .bin_lt, + .bin_gt, + .bin_le, + .bin_ge, + .bin_instanceof, + .bin_in, + => break :brk PrimitiveType.boolean, + .bin_logical_or, .bin_logical_and => break :brk binary.left.data.mergeKnownPrimitive(binary.right.data), + + .bin_nullish_coalescing => { + const left = binary.left.data.knownPrimitive(); + const right = binary.right.data.knownPrimitive(); + if (left == .null or left == .undefined) + break :brk right; + + if (left != .unknown) { + if (left != .mixed) + break :brk left; // Definitely not null or undefined + + if (right != .unknown) + break :brk PrimitiveType.mixed; // Definitely some kind of primitive + } + }, + + .bin_add => { + const left = binary.left.data.knownPrimitive(); + const right = binary.right.data.knownPrimitive(); + + if (left == .string or right == .string) + break :brk PrimitiveType.string; + + if (left == .bigint or right == .bigint) + break :brk PrimitiveType.bigint; + + if (switch (left) { + .unknown, .mixed, .bigint => false, + else => true, + } and switch (right) { + .unknown, .mixed, .bigint => false, + else => true, + }) + break :brk PrimitiveType.number; + + break :brk PrimitiveType.mixed; // Can be number or bigint or string (or an exception) + }, + + .bin_sub, + .bin_sub_assign, + .bin_mul, + .bin_mul_assign, + .bin_div, + .bin_div_assign, + .bin_rem, + .bin_rem_assign, + .bin_pow, + .bin_pow_assign, + .bin_bitwise_and, + .bin_bitwise_and_assign, + .bin_bitwise_or, + .bin_bitwise_or_assign, + .bin_bitwise_xor, + .bin_bitwise_xor_assign, + .bin_shl, + .bin_shl_assign, + .bin_shr, + .bin_shr_assign, + .bin_u_shr, + .bin_u_shr_assign, + => break :brk PrimitiveType.mixed, // Can be number or bigint (or an exception) + + .bin_assign, + .bin_comma, + => break :brk binary.right.data.knownPrimitive(), + + else => {}, + } + + break :brk PrimitiveType.unknown; + }, + + .e_unary => switch (data.e_unary.op) { + .un_void => PrimitiveType.undefined, + .un_typeof => PrimitiveType.string, + .un_not, .un_delete => PrimitiveType.boolean, + .un_pos => PrimitiveType.number, // Cannot be bigint because that throws an exception + .un_neg, .un_cpl => switch (data.e_unary.value.data.knownPrimitive()) { + .bigint => PrimitiveType.bigint, + .unknown, .mixed => PrimitiveType.mixed, + else => PrimitiveType.number, // Can be number or bigint + }, + .un_pre_dec, .un_pre_inc, .un_post_dec, .un_post_inc => PrimitiveType.mixed, // Can be number or bigint + + else => PrimitiveType.unknown, + }, + + .e_inlined_enum => |inlined| inlined.value.data.knownPrimitive(), + + else => PrimitiveType.unknown, + }; + } + + pub fn mergeKnownPrimitive(lhs: Expr.Data, rhs: Expr.Data) PrimitiveType { + return lhs.knownPrimitive().merge(rhs.knownPrimitive()); + } + + /// Returns true if the result of the "typeof" operator on this expression is + /// statically determined and this expression has no side effects (i.e. can be + /// removed without consequence). + pub inline fn toTypeof(data: Expr.Data) ?string { + return @as(Expr.Tag, data).typeof(); + } + + pub fn toNumber(data: Expr.Data) ?f64 { + return switch (data) { + .e_null => 0, + .e_undefined => std.math.nan(f64), + .e_string => |str| { + if (str.next != null) return null; + if (!str.isUTF8()) return null; + + // +'1' => 1 + return stringToEquivalentNumberValue(str.slice8()); + }, + .e_boolean => @as(f64, if (data.e_boolean.value) 1.0 else 0.0), + .e_number => data.e_number.value, + .e_inlined_enum => |inlined| switch (inlined.value.data) { + .e_number => |num| num.value, + .e_string => |str| { + if (str.next != null) return null; + if (!str.isUTF8()) return null; + + // +'1' => 1 + return stringToEquivalentNumberValue(str.slice8()); + }, + else => null, + }, + else => null, + }; + } + + pub fn toFiniteNumber(data: Expr.Data) ?f64 { + return switch (data) { + .e_boolean => @as(f64, if (data.e_boolean.value) 1.0 else 0.0), + .e_number => if (std.math.isFinite(data.e_number.value)) + data.e_number.value + else + null, + .e_inlined_enum => |inlined| switch (inlined.value.data) { + .e_number => |num| if (std.math.isFinite(num.value)) + num.value + else + null, + else => null, + }, + else => null, + }; + } + + pub fn extractNumericValue(data: Expr.Data) ?f64 { + return switch (data) { + .e_number => data.e_number.value, + .e_inlined_enum => |inlined| switch (inlined.value.data) { + .e_number => |num| num.value, + else => null, + }, + else => null, + }; + } + + pub const Equality = struct { + equal: bool = false, + ok: bool = false, + + /// This extra flag is unfortunately required for the case of visiting the expression + /// `require.main === module` (and any combination of !==, ==, !=, either ordering) + /// + /// We want to replace this with the dedicated import_meta_main node, which: + /// - Stops this module from having p.require_ref, allowing conversion to ESM + /// - Allows us to inline `import.meta.main`'s value, if it is known (bun build --compile) + is_require_main_and_module: bool = false, + + pub const @"true" = Equality{ .ok = true, .equal = true }; + pub const @"false" = Equality{ .ok = true, .equal = false }; + pub const unknown = Equality{ .ok = false }; + }; + + // Returns "equal, ok". If "ok" is false, then nothing is known about the two + // values. If "ok" is true, the equality or inequality of the two values is + // stored in "equal". + pub fn eql( + left: Expr.Data, + right: Expr.Data, + p: anytype, + comptime kind: enum { loose, strict }, + ) Equality { + comptime bun.assert(@typeInfo(@TypeOf(p)).pointer.size == .one); // pass *Parser + + // https://dorey.github.io/JavaScript-Equality-Table/ + switch (left) { + .e_inlined_enum => |inlined| return inlined.value.data.eql(right, p, kind), + + .e_null, .e_undefined => { + const ok = switch (@as(Expr.Tag, right)) { + .e_null, .e_undefined => true, + else => @as(Expr.Tag, right).isPrimitiveLiteral(), + }; + + if (comptime kind == .loose) { + return .{ + .equal = switch (@as(Expr.Tag, right)) { + .e_null, .e_undefined => true, + else => false, + }, + .ok = ok, + }; + } + + return .{ + .equal = @as(Tag, right) == @as(Tag, left), + .ok = ok, + }; + }, + .e_boolean => |l| { + switch (right) { + .e_boolean => { + return .{ + .ok = true, + .equal = l.value == right.e_boolean.value, + }; + }, + .e_number => |num| { + if (comptime kind == .strict) { + // "true === 1" is false + // "false === 0" is false + return Equality.false; + } + + return .{ + .ok = true, + .equal = if (l.value) + num.value == 1 + else + num.value == 0, + }; + }, + .e_null, .e_undefined => { + return Equality.false; + }, + else => {}, + } + }, + .e_number => |l| { + switch (right) { + .e_number => |r| { + return .{ + .ok = true, + .equal = l.value == r.value, + }; + }, + .e_inlined_enum => |r| if (r.value.data == .e_number) { + return .{ + .ok = true, + .equal = l.value == r.value.data.e_number.value, + }; + }, + .e_boolean => |r| { + if (comptime kind == .loose) { + return .{ + .ok = true, + // "1 == true" is true + // "0 == false" is true + .equal = if (r.value) + l.value == 1 + else + l.value == 0, + }; + } + + // "1 === true" is false + // "0 === false" is false + return Equality.false; + }, + .e_null, .e_undefined => { + // "(not null or undefined) == undefined" is false + return Equality.false; + }, + else => {}, + } + }, + .e_big_int => |l| { + if (right == .e_big_int) { + if (strings.eqlLong(l.value, right.e_big_int.value, true)) { + return Equality.true; + } + + // 0x0000n == 0n is true + return .{ .ok = false }; + } else { + return .{ + .ok = switch (right) { + .e_null, .e_undefined => true, + else => false, + }, + .equal = false, + }; + } + }, + .e_string => |l| { + switch (right) { + .e_string => |r| { + r.resolveRopeIfNeeded(p.allocator); + l.resolveRopeIfNeeded(p.allocator); + return .{ + .ok = true, + .equal = r.eql(E.String, l), + }; + }, + .e_inlined_enum => |inlined| { + if (inlined.value.data == .e_string) { + const r = inlined.value.data.e_string; + + r.resolveRopeIfNeeded(p.allocator); + l.resolveRopeIfNeeded(p.allocator); + + return .{ + .ok = true, + .equal = r.eql(E.String, l), + }; + } + }, + .e_null, .e_undefined => { + return Equality.false; + }, + .e_number => |r| { + if (comptime kind == .loose) { + l.resolveRopeIfNeeded(p.allocator); + if (r.value == 0 and (l.isBlank() or l.eqlComptime("0"))) { + return Equality.true; + } + + if (r.value == 1 and l.eqlComptime("1")) { + return Equality.true; + } + + // the string could still equal 0 or 1 but it could be hex, binary, octal, ... + return Equality.unknown; + } else { + return Equality.false; + } + }, + + else => {}, + } + }, + + else => { + // Do not need to check left because e_require_main is + // always re-ordered to the right side. + if (right == .e_require_main) { + if (left.as(.e_identifier)) |id| { + if (id.ref.eql(p.module_ref)) return .{ + .ok = true, + .equal = true, + .is_require_main_and_module = true, + }; + } + } + }, + } + + return Equality.unknown; + } + + pub fn toJS(this: Data, allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) ToJSError!JSC.JSValue { + return switch (this) { + .e_array => |e| e.toJS(allocator, globalObject), + .e_object => |e| e.toJS(allocator, globalObject), + .e_string => |e| e.toJS(allocator, globalObject), + .e_null => JSC.JSValue.null, + .e_undefined => .js_undefined, + .e_boolean => |boolean| if (boolean.value) + JSC.JSValue.true + else + JSC.JSValue.false, + .e_number => |e| e.toJS(), + // .e_big_int => |e| e.toJS(ctx, exception), + + .e_inlined_enum => |inlined| inlined.value.data.toJS(allocator, globalObject), + + .e_identifier, + .e_import_identifier, + .e_private_identifier, + .e_commonjs_export_identifier, + => error.@"Cannot convert identifier to JS. Try a statically-known value", + + // brk: { + // // var node = try allocator.create(Macro.JSNode); + // // node.* = Macro.JSNode.initExpr(Expr{ .data = this, .loc = logger.Loc.Empty }); + // // break :brk JSC.JSValue.c(Macro.JSNode.Class.make(globalObject, node)); + // }, + + else => { + return error.@"Cannot convert argument type to JS"; + }, + }; + } + + pub const Store = struct { + const StoreType = NewStore(&.{ + E.NameOfSymbol, + E.Array, + E.Arrow, + E.Await, + E.BigInt, + E.Binary, + E.Call, + E.Class, + E.Dot, + E.Function, + E.If, + E.Import, + E.Index, + E.InlinedEnum, + E.JSXElement, + E.New, + E.Number, + E.Object, + E.PrivateIdentifier, + E.RegExp, + E.Spread, + E.String, + E.Template, + E.TemplatePart, + E.Unary, + E.Yield, + }, 512); + + pub threadlocal var instance: ?*StoreType = null; + pub threadlocal var memory_allocator: ?*ASTMemoryAllocator = null; + pub threadlocal var disable_reset = false; + + pub fn create() void { + if (instance != null or memory_allocator != null) { + return; + } + + instance = StoreType.init(); + } + + pub fn reset() void { + if (disable_reset or memory_allocator != null) return; + instance.?.reset(); + } + + pub fn deinit() void { + if (instance == null or memory_allocator != null) return; + instance.?.deinit(); + instance = null; + } + + pub inline fn assert() void { + if (comptime Environment.isDebug or Environment.enable_asan) { + if (instance == null and memory_allocator == null) + bun.unreachablePanic("Store must be init'd", .{}); + } + } + + /// create || reset + pub fn begin() void { + if (memory_allocator != null) return; + if (instance == null) { + create(); + return; + } + + if (!disable_reset) + instance.?.reset(); + } + + pub fn append(comptime T: type, value: T) *T { + if (memory_allocator) |allocator| { + return allocator.append(T, value); + } + + Disabler.assert(); + return instance.?.append(T, value); + } + }; + + pub inline fn isStringValue(self: Data) bool { + return @as(Expr.Tag, self) == .e_string; + } +}; + +pub fn StoredData(tag: Tag) type { + const T = @FieldType(Data, tag); + return switch (@typeInfo(T)) { + .pointer => |ptr| ptr.child, + else => T, + }; +} + +extern fn JSC__jsToNumber(latin1_ptr: [*]const u8, len: usize) f64; + +fn stringToEquivalentNumberValue(str: []const u8) f64 { + // +"" -> 0 + if (str.len == 0) return 0; + if (!bun.strings.isAllASCII(str)) + return std.math.nan(f64); + return JSC__jsToNumber(str.ptr, str.len); +} + +// @sortImports + +const JSPrinter = @import("../js_printer.zig"); +const std = @import("std"); + +const bun = @import("bun"); +const BabyList = bun.BabyList; +const Environment = bun.Environment; +const JSC = bun.JSC; +const JSONParser = bun.JSON; +const MutableString = bun.MutableString; +const OOM = bun.OOM; +const default_allocator = bun.default_allocator; +const logger = bun.logger; +const string = bun.string; +const stringZ = bun.stringZ; +const strings = bun.strings; +const writeAnyToHasher = bun.writeAnyToHasher; +const MimeType = bun.http.MimeType; + +const js_ast = bun.js_ast; +const ASTMemoryAllocator = js_ast.ASTMemoryAllocator; +const E = js_ast.E; +const Expr = js_ast.Expr; +const G = js_ast.G; +const NewStore = js_ast.NewStore; +const Op = js_ast.Op; +const Ref = js_ast.Ref; +const S = js_ast.S; +const Stmt = js_ast.Stmt; +const ToJSError = js_ast.ToJSError; diff --git a/src/ast/G.zig b/src/ast/G.zig new file mode 100644 index 0000000000..8b1a2a2fd9 --- /dev/null +++ b/src/ast/G.zig @@ -0,0 +1,231 @@ +pub const Decl = struct { + binding: BindingNodeIndex, + value: ?ExprNodeIndex = null, + + pub const List = BabyList(Decl); +}; + +pub const NamespaceAlias = struct { + namespace_ref: Ref, + alias: string, + + was_originally_property_access: bool = false, + + import_record_index: u32 = std.math.maxInt(u32), +}; + +pub const ExportStarAlias = struct { + loc: logger.Loc, + + // Although this alias name starts off as being the same as the statement's + // namespace symbol, it may diverge if the namespace symbol name is minified. + // The original alias name is preserved here to avoid this scenario. + original_name: string, +}; + +pub const Class = struct { + class_keyword: logger.Range = logger.Range.None, + ts_decorators: ExprNodeList = ExprNodeList{}, + class_name: ?LocRef = null, + extends: ?ExprNodeIndex = null, + body_loc: logger.Loc = logger.Loc.Empty, + close_brace_loc: logger.Loc = logger.Loc.Empty, + properties: []Property = &([_]Property{}), + has_decorators: bool = false, + + pub fn canBeMoved(this: *const Class) bool { + if (this.extends != null) + return false; + + if (this.has_decorators) { + return false; + } + + for (this.properties) |property| { + if (property.kind == .class_static_block) + return false; + + const flags = property.flags; + if (flags.contains(.is_computed) or flags.contains(.is_spread)) { + return false; + } + + if (property.kind == .normal) { + if (flags.contains(.is_static)) { + for ([2]?Expr{ property.value, property.initializer }) |val_| { + if (val_) |val| { + switch (val.data) { + .e_arrow, .e_function => {}, + else => { + if (!val.canBeMoved()) { + return false; + } + }, + } + } + } + } + } + } + + return true; + } +}; + +// invalid shadowing if left as Comment +pub const Comment = struct { loc: logger.Loc, text: string }; + +pub const ClassStaticBlock = struct { + stmts: BabyList(Stmt) = .{}, + loc: logger.Loc, +}; + +pub const Property = struct { + /// This is used when parsing a pattern that uses default values: + /// + /// [a = 1] = []; + /// ({a = 1} = {}); + /// + /// It's also used for class fields: + /// + /// class Foo { a = 1 } + /// + initializer: ?ExprNodeIndex = null, + kind: Kind = .normal, + flags: Flags.Property.Set = Flags.Property.None, + + class_static_block: ?*ClassStaticBlock = null, + ts_decorators: ExprNodeList = .{}, + // Key is optional for spread + key: ?ExprNodeIndex = null, + + // This is omitted for class fields + value: ?ExprNodeIndex = null, + + ts_metadata: TypeScript.Metadata = .m_none, + + pub const List = BabyList(Property); + + pub fn deepClone(this: *const Property, allocator: std.mem.Allocator) !Property { + var class_static_block: ?*ClassStaticBlock = null; + if (this.class_static_block != null) { + class_static_block = bun.create(allocator, ClassStaticBlock, .{ + .loc = this.class_static_block.?.loc, + .stmts = try this.class_static_block.?.stmts.clone(allocator), + }); + } + return .{ + .initializer = if (this.initializer) |init| try init.deepClone(allocator) else null, + .kind = this.kind, + .flags = this.flags, + .class_static_block = class_static_block, + .ts_decorators = try this.ts_decorators.deepClone(allocator), + .key = if (this.key) |key| try key.deepClone(allocator) else null, + .value = if (this.value) |value| try value.deepClone(allocator) else null, + .ts_metadata = this.ts_metadata, + }; + } + + pub const Kind = enum(u3) { + normal, + get, + set, + spread, + declare, + abstract, + class_static_block, + + pub fn jsonStringify(self: @This(), writer: anytype) !void { + return try writer.write(@tagName(self)); + } + }; +}; + +pub const FnBody = struct { + loc: logger.Loc, + stmts: StmtNodeList, + + pub fn initReturnExpr(allocator: std.mem.Allocator, expr: Expr) !FnBody { + return .{ + .stmts = try allocator.dupe(Stmt, &.{Stmt.alloc(S.Return, .{ + .value = expr, + }, expr.loc)}), + .loc = expr.loc, + }; + } +}; + +pub const Fn = struct { + name: ?LocRef = null, + open_parens_loc: logger.Loc = logger.Loc.Empty, + args: []Arg = &.{}, + // This was originally nullable, but doing so I believe caused a miscompilation + // Specifically, the body was always null. + body: FnBody = .{ .loc = logger.Loc.Empty, .stmts = &.{} }, + arguments_ref: ?Ref = null, + + flags: Flags.Function.Set = Flags.Function.None, + + return_ts_metadata: TypeScript.Metadata = .m_none, + + pub fn deepClone(this: *const Fn, allocator: std.mem.Allocator) !Fn { + const args = try allocator.alloc(Arg, this.args.len); + for (0..args.len) |i| { + args[i] = try this.args[i].deepClone(allocator); + } + return .{ + .name = this.name, + .open_parens_loc = this.open_parens_loc, + .args = args, + .body = .{ + .loc = this.body.loc, + .stmts = this.body.stmts, + }, + .arguments_ref = this.arguments_ref, + .flags = this.flags, + .return_ts_metadata = this.return_ts_metadata, + }; + } +}; +pub const Arg = struct { + ts_decorators: ExprNodeList = ExprNodeList{}, + binding: BindingNodeIndex, + default: ?ExprNodeIndex = null, + + // "constructor(public x: boolean) {}" + is_typescript_ctor_field: bool = false, + + ts_metadata: TypeScript.Metadata = .m_none, + + pub fn deepClone(this: *const Arg, allocator: std.mem.Allocator) !Arg { + return .{ + .ts_decorators = try this.ts_decorators.deepClone(allocator), + .binding = this.binding, + .default = if (this.default) |d| try d.deepClone(allocator) else null, + .is_typescript_ctor_field = this.is_typescript_ctor_field, + .ts_metadata = this.ts_metadata, + }; + } +}; + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const BabyList = bun.BabyList; +const logger = bun.logger; +const string = bun.string; +const TypeScript = bun.js_parser.TypeScript; + +const js_ast = bun.js_ast; +const BindingNodeIndex = js_ast.BindingNodeIndex; +const Expr = js_ast.Expr; +const ExprNodeIndex = js_ast.ExprNodeIndex; +const ExprNodeList = js_ast.ExprNodeList; +const Flags = js_ast.Flags; +const LocRef = js_ast.LocRef; +const Ref = js_ast.Ref; +const S = js_ast.S; +const Stmt = js_ast.Stmt; +const StmtNodeList = js_ast.StmtNodeList; diff --git a/src/ast/Macro.zig b/src/ast/Macro.zig new file mode 100644 index 0000000000..1a7f06c363 --- /dev/null +++ b/src/ast/Macro.zig @@ -0,0 +1,669 @@ +pub const namespace: string = "macro"; +pub const namespaceWithColon: string = namespace ++ ":"; + +pub fn isMacroPath(str: string) bool { + return strings.hasPrefixComptime(str, namespaceWithColon); +} + +pub const MacroContext = struct { + pub const MacroMap = std.AutoArrayHashMap(i32, Macro); + + resolver: *Resolver, + env: *DotEnv.Loader, + macros: MacroMap, + remap: MacroRemap, + javascript_object: JSC.JSValue = JSC.JSValue.zero, + + pub fn getRemap(this: MacroContext, path: string) ?MacroRemapEntry { + if (this.remap.entries.len == 0) return null; + return this.remap.get(path); + } + + pub fn init(transpiler: *Transpiler) MacroContext { + return MacroContext{ + .macros = MacroMap.init(default_allocator), + .resolver = &transpiler.resolver, + .env = transpiler.env, + .remap = transpiler.options.macro_remap, + }; + } + + pub fn call( + this: *MacroContext, + import_record_path: string, + source_dir: string, + log: *logger.Log, + source: *const logger.Source, + import_range: logger.Range, + caller: Expr, + function_name: string, + ) anyerror!Expr { + Expr.Data.Store.disable_reset = true; + Stmt.Data.Store.disable_reset = true; + defer Expr.Data.Store.disable_reset = false; + defer Stmt.Data.Store.disable_reset = false; + // const is_package_path = isPackagePath(specifier); + const import_record_path_without_macro_prefix = if (isMacroPath(import_record_path)) + import_record_path[namespaceWithColon.len..] + else + import_record_path; + + bun.assert(!isMacroPath(import_record_path_without_macro_prefix)); + + const input_specifier = brk: { + if (JSC.ModuleLoader.HardcodedModule.Alias.get(import_record_path, .bun)) |replacement| { + break :brk replacement.path; + } + + const resolve_result = this.resolver.resolve(source_dir, import_record_path_without_macro_prefix, .stmt) catch |err| { + switch (err) { + error.ModuleNotFound => { + log.addResolveError( + source, + import_range, + log.msgs.allocator, + "Macro \"{s}\" not found", + .{import_record_path}, + .stmt, + err, + ) catch unreachable; + return error.MacroNotFound; + }, + else => { + log.addRangeErrorFmt( + source, + import_range, + log.msgs.allocator, + "{s} resolving macro \"{s}\"", + .{ @errorName(err), import_record_path }, + ) catch unreachable; + return err; + }, + } + }; + break :brk resolve_result.path_pair.primary.text; + }; + + var specifier_buf: [64]u8 = undefined; + var specifier_buf_len: u32 = 0; + const hash = MacroEntryPoint.generateID( + input_specifier, + function_name, + &specifier_buf, + &specifier_buf_len, + ); + + const macro_entry = this.macros.getOrPut(hash) catch unreachable; + if (!macro_entry.found_existing) { + macro_entry.value_ptr.* = Macro.init( + default_allocator, + this.resolver, + input_specifier, + log, + this.env, + function_name, + specifier_buf[0..specifier_buf_len], + hash, + ) catch |err| { + macro_entry.value_ptr.* = Macro{ .resolver = undefined, .disabled = true }; + return err; + }; + Output.flush(); + } + defer Output.flush(); + + const macro = macro_entry.value_ptr.*; + if (macro.disabled) { + return caller; + } + macro.vm.enableMacroMode(); + defer macro.vm.disableMacroMode(); + + const Wrapper = struct { + args: std.meta.ArgsTuple(@TypeOf(Macro.Runner.run)), + ret: Runner.MacroError!Expr, + + pub fn call(self: *@This()) void { + self.ret = @call(.auto, Macro.Runner.run, self.args); + } + }; + var wrapper = Wrapper{ + .args = .{ + macro, + log, + default_allocator, + function_name, + caller, + source, + hash, + this.javascript_object, + }, + .ret = undefined, + }; + + macro.vm.runWithAPILock(Wrapper, &wrapper, Wrapper.call); + return try wrapper.ret; + // this.macros.getOrPut(key: K) + } +}; + +pub const MacroResult = struct { + import_statements: []S.Import = &[_]S.Import{}, + replacement: Expr, +}; + +resolver: *Resolver, +vm: *JavaScript.VirtualMachine = undefined, + +resolved: ResolveResult = undefined, +disabled: bool = false, + +pub fn init( + _: std.mem.Allocator, + resolver: *Resolver, + input_specifier: []const u8, + log: *logger.Log, + env: *DotEnv.Loader, + function_name: string, + specifier: string, + hash: i32, +) !Macro { + var vm: *JavaScript.VirtualMachine = if (JavaScript.VirtualMachine.isLoaded()) + JavaScript.VirtualMachine.get() + else brk: { + const old_transform_options = resolver.opts.transform_options; + defer resolver.opts.transform_options = old_transform_options; + + // JSC needs to be initialized if building from CLI + JSC.initialize(false); + + var _vm = try JavaScript.VirtualMachine.init(.{ + .allocator = default_allocator, + .args = resolver.opts.transform_options, + .log = log, + .is_main_thread = false, + .env_loader = env, + }); + + _vm.enableMacroMode(); + _vm.eventLoop().ensureWaker(); + + try _vm.transpiler.configureDefines(); + break :brk _vm; + }; + + vm.enableMacroMode(); + + const loaded_result = try vm.loadMacroEntryPoint(input_specifier, function_name, specifier, hash); + + switch (loaded_result.unwrap(vm.jsc, .leave_unhandled)) { + .rejected => |result| { + vm.unhandledRejection(vm.global, result, loaded_result.asValue()); + vm.disableMacroMode(); + return error.MacroLoadError; + }, + else => {}, + } + + return Macro{ + .vm = vm, + .resolver = resolver, + }; +} + +pub const Runner = struct { + const VisitMap = std.AutoHashMapUnmanaged(JSC.JSValue, Expr); + + threadlocal var args_buf: [3]js.JSObjectRef = undefined; + threadlocal var exception_holder: JSC.ZigException.Holder = undefined; + pub const MacroError = error{ MacroFailed, OutOfMemory } || ToJSError || bun.JSError; + + pub const Run = struct { + caller: Expr, + function_name: string, + macro: *const Macro, + global: *JSC.JSGlobalObject, + allocator: std.mem.Allocator, + id: i32, + log: *logger.Log, + source: *const logger.Source, + visited: VisitMap = VisitMap{}, + is_top_level: bool = false, + + pub fn runAsync( + macro: Macro, + log: *logger.Log, + allocator: std.mem.Allocator, + function_name: string, + caller: Expr, + args: []JSC.JSValue, + source: *const logger.Source, + id: i32, + ) MacroError!Expr { + const macro_callback = macro.vm.macros.get(id) orelse return caller; + + const result = js.JSObjectCallAsFunctionReturnValueHoldingAPILock( + macro.vm.global, + macro_callback, + null, + args.len, + @as([*]js.JSObjectRef, @ptrCast(args.ptr)), + ); + + var runner = Run{ + .caller = caller, + .function_name = function_name, + .macro = ¯o, + .allocator = allocator, + .global = macro.vm.global, + .id = id, + .log = log, + .source = source, + .visited = VisitMap{}, + }; + + defer runner.visited.deinit(allocator); + + return try runner.run( + result, + ); + } + + pub fn run( + this: *Run, + value: JSC.JSValue, + ) MacroError!Expr { + return switch ((try JSC.ConsoleObject.Formatter.Tag.get(value, this.global)).tag) { + .Error => this.coerce(value, .Error), + .Undefined => this.coerce(value, .Undefined), + .Null => this.coerce(value, .Null), + .Private => this.coerce(value, .Private), + .Boolean => this.coerce(value, .Boolean), + .Array => this.coerce(value, .Array), + .Object => this.coerce(value, .Object), + .toJSON, .JSON => this.coerce(value, .JSON), + .Integer => this.coerce(value, .Integer), + .Double => this.coerce(value, .Double), + .String => this.coerce(value, .String), + .Promise => this.coerce(value, .Promise), + else => brk: { + const name = value.getClassInfoName() orelse "unknown"; + + this.log.addErrorFmt( + this.source, + this.caller.loc, + this.allocator, + "cannot coerce {s} ({s}) to Bun's AST. Please return a simpler type", + .{ name, @tagName(value.jsType()) }, + ) catch unreachable; + break :brk error.MacroFailed; + }, + }; + } + + pub fn coerce( + this: *Run, + value: JSC.JSValue, + comptime tag: JSC.ConsoleObject.Formatter.Tag, + ) MacroError!Expr { + switch (comptime tag) { + .Error => { + _ = this.macro.vm.uncaughtException(this.global, value, false); + return this.caller; + }, + .Undefined => if (this.is_top_level) + return this.caller + else + return Expr.init(E.Undefined, E.Undefined{}, this.caller.loc), + .Null => return Expr.init(E.Null, E.Null{}, this.caller.loc), + .Private => { + this.is_top_level = false; + const _entry = this.visited.getOrPut(this.allocator, value) catch unreachable; + if (_entry.found_existing) { + return _entry.value_ptr.*; + } + + var blob_: ?JSC.WebCore.Blob = null; + const mime_type: ?MimeType = null; + + if (value.jsType() == .DOMWrapper) { + if (value.as(JSC.WebCore.Response)) |resp| { + return this.run(try resp.getBlobWithoutCallFrame(this.global)); + } else if (value.as(JSC.WebCore.Request)) |resp| { + return this.run(try resp.getBlobWithoutCallFrame(this.global)); + } else if (value.as(JSC.WebCore.Blob)) |resp| { + blob_ = resp.*; + blob_.?.allocator = null; + } else if (value.as(bun.api.ResolveMessage) != null or value.as(bun.api.BuildMessage) != null) { + _ = this.macro.vm.uncaughtException(this.global, value, false); + return error.MacroFailed; + } + } + + if (blob_) |*blob| { + const out_expr = Expr.fromBlob( + blob, + this.allocator, + mime_type, + this.log, + this.caller.loc, + ) catch { + blob.deinit(); + return error.MacroFailed; + }; + if (out_expr.data == .e_string) { + blob.deinit(); + } + + return out_expr; + } + + return Expr.init(E.String, E.String.empty, this.caller.loc); + }, + + .Boolean => { + return Expr{ .data = .{ .e_boolean = .{ .value = value.toBoolean() } }, .loc = this.caller.loc }; + }, + JSC.ConsoleObject.Formatter.Tag.Array => { + this.is_top_level = false; + + const _entry = this.visited.getOrPut(this.allocator, value) catch unreachable; + if (_entry.found_existing) { + switch (_entry.value_ptr.*.data) { + .e_object, .e_array => { + this.log.addErrorFmt(this.source, this.caller.loc, this.allocator, "converting circular structure to Bun AST is not implemented yet", .{}) catch unreachable; + return error.MacroFailed; + }, + else => {}, + } + return _entry.value_ptr.*; + } + + var iter = try JSC.JSArrayIterator.init(value, this.global); + if (iter.len == 0) { + const result = Expr.init( + E.Array, + E.Array{ + .items = ExprNodeList.init(&[_]Expr{}), + .was_originally_macro = true, + }, + this.caller.loc, + ); + _entry.value_ptr.* = result; + return result; + } + var array = this.allocator.alloc(Expr, iter.len) catch unreachable; + var out = Expr.init( + E.Array, + E.Array{ + .items = ExprNodeList.init(array[0..0]), + .was_originally_macro = true, + }, + this.caller.loc, + ); + _entry.value_ptr.* = out; + + errdefer this.allocator.free(array); + var i: usize = 0; + while (try iter.next()) |item| { + array[i] = try this.run(item); + if (array[i].isMissing()) + continue; + i += 1; + } + out.data.e_array.items = ExprNodeList.init(array); + _entry.value_ptr.* = out; + return out; + }, + // TODO: optimize this + JSC.ConsoleObject.Formatter.Tag.Object => { + this.is_top_level = false; + const _entry = this.visited.getOrPut(this.allocator, value) catch unreachable; + if (_entry.found_existing) { + switch (_entry.value_ptr.*.data) { + .e_object, .e_array => { + this.log.addErrorFmt(this.source, this.caller.loc, this.allocator, "converting circular structure to Bun AST is not implemented yet", .{}) catch unreachable; + return error.MacroFailed; + }, + else => {}, + } + return _entry.value_ptr.*; + } + // SAFETY: tag ensures `value` is an object. + const obj = value.getObject() orelse unreachable; + var object_iter = try JSC.JSPropertyIterator(.{ + .skip_empty_name = false, + .include_value = true, + }).init(this.global, obj); + defer object_iter.deinit(); + var properties = this.allocator.alloc(G.Property, object_iter.len) catch unreachable; + errdefer this.allocator.free(properties); + var out = Expr.init( + E.Object, + E.Object{ + .properties = BabyList(G.Property).init(properties), + .was_originally_macro = true, + }, + this.caller.loc, + ); + _entry.value_ptr.* = out; + + while (try object_iter.next()) |prop| { + properties[object_iter.i] = G.Property{ + .key = Expr.init(E.String, E.String.init(prop.toOwnedSlice(this.allocator) catch unreachable), this.caller.loc), + .value = try this.run(object_iter.value), + }; + } + out.data.e_object.properties = BabyList(G.Property).init(properties[0..object_iter.i]); + _entry.value_ptr.* = out; + return out; + }, + + .JSON => { + this.is_top_level = false; + // 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(this.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 = @as(f64, @floatFromInt(value.toInt32())) }, this.caller.loc); + }, + .Double => { + return Expr.init(E.Number, E.Number{ .value = value.asNumber() }, this.caller.loc); + }, + .String => { + var bun_str = try value.toBunString(this.global); + defer bun_str.deref(); + + // encode into utf16 so the printer escapes the string correctly + var utf16_bytes = this.allocator.alloc(u16, bun_str.length()) catch unreachable; + const out_slice = utf16_bytes[0 .. (bun_str.encodeInto(std.mem.sliceAsBytes(utf16_bytes), .utf16le) catch 0) / 2]; + return Expr.init(E.String, E.String.init(out_slice), this.caller.loc); + }, + .Promise => { + const _entry = this.visited.getOrPut(this.allocator, value) catch unreachable; + if (_entry.found_existing) { + return _entry.value_ptr.*; + } + + const promise = value.asAnyPromise() orelse @panic("Unexpected promise type"); + + this.macro.vm.waitForPromise(promise); + + const promise_result = promise.result(this.macro.vm.jsc); + const rejected = promise.status(this.macro.vm.jsc) == .rejected; + + if (promise_result.isUndefined() and this.is_top_level) { + this.is_top_level = false; + return this.caller; + } + + if (rejected or promise_result.isError() or promise_result.isAggregateError(this.global) or promise_result.isException(this.global.vm())) { + this.macro.vm.unhandledRejection(this.global, promise_result, promise.asValue()); + return error.MacroFailed; + } + this.is_top_level = false; + const result = try this.run(promise_result); + + _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 simpler type", + .{@tagName(value.jsType())}, + ) catch unreachable; + return error.MacroFailed; + } + }; + + pub fn run( + macro: Macro, + log: *logger.Log, + allocator: std.mem.Allocator, + function_name: string, + caller: Expr, + source: *const logger.Source, + id: i32, + javascript_object: JSC.JSValue, + ) MacroError!Expr { + if (comptime Environment.isDebug) Output.prettyln("[macro] call {s}", .{function_name}); + + exception_holder = JSC.ZigException.Holder.init(); + var js_args: []JSC.JSValue = &.{}; + var js_processed_args_len: usize = 0; + defer { + for (js_args[0..js_processed_args_len -| @as(usize, @intFromBool(javascript_object != .zero))]) |arg| { + arg.unprotect(); + } + + allocator.free(js_args); + } + + const globalObject = JSC.VirtualMachine.get().global; + + switch (caller.data) { + .e_call => |call| { + const call_args: []Expr = call.args.slice(); + js_args = try allocator.alloc(JSC.JSValue, call_args.len + @as(usize, @intFromBool(javascript_object != .zero))); + js_processed_args_len = js_args.len; + + for (0.., call_args, js_args[0..call_args.len]) |i, in, *out| { + const value = in.toJS( + allocator, + globalObject, + ) catch |e| { + // Keeping a separate variable instead of modifying js_args.len + // due to allocator.free call in defer + js_processed_args_len = i; + return e; + }; + value.protect(); + out.* = value; + } + }, + .e_template => { + @panic("TODO: support template literals in macros"); + }, + else => { + @panic("Unexpected caller type"); + }, + } + + if (javascript_object != .zero) { + if (js_args.len == 0) { + js_args = try allocator.alloc(JSC.JSValue, 1); + } + + js_args[js_args.len - 1] = javascript_object; + } + + const CallFunction = @TypeOf(Run.runAsync); + const CallArgs = std.meta.ArgsTuple(CallFunction); + const CallData = struct { + threadlocal var call_args: CallArgs = undefined; + threadlocal var result: MacroError!Expr = undefined; + pub fn callWrapper(args: CallArgs) MacroError!Expr { + JSC.markBinding(@src()); + call_args = args; + Bun__startMacro(&call, JSC.VirtualMachine.get().global); + return result; + } + + pub fn call() callconv(.C) void { + const call_args_copy = call_args; + const local_result = @call(.auto, Run.runAsync, call_args_copy); + result = local_result; + } + }; + + // TODO: can change back to `return CallData.callWrapper(.{` + // when https://github.com/ziglang/zig/issues/16242 is fixed + return CallData.callWrapper(CallArgs{ + macro, + log, + allocator, + function_name, + caller, + js_args, + source, + id, + }); + } + + extern "c" fn Bun__startMacro(function: *const anyopaque, *anyopaque) void; +}; + +// @sortImports + +const DotEnv = @import("../env_loader.zig"); +const std = @import("std"); + +const MacroRemap = @import("../resolver/package_json.zig").MacroMap; +const MacroRemapEntry = @import("../resolver/package_json.zig").MacroImportReplacementMap; + +const ResolveResult = @import("../resolver/resolver.zig").Result; +const Resolver = @import("../resolver/resolver.zig").Resolver; +const isPackagePath = @import("../resolver/resolver.zig").isPackagePath; + +const bun = @import("bun"); +const BabyList = bun.BabyList; +const Environment = bun.Environment; +const Output = bun.Output; +const Transpiler = bun.Transpiler; +const default_allocator = bun.default_allocator; +const logger = bun.logger; +const string = bun.string; +const strings = bun.strings; +const Loader = bun.options.Loader; +const MimeType = bun.http.MimeType; +const MacroEntryPoint = bun.transpiler.EntryPoints.MacroEntryPoint; + +const JSC = bun.JSC; +const JavaScript = bun.JSC; +const js = bun.JSC.C; + +const js_ast = bun.js_ast; +const E = js_ast.E; +const Expr = js_ast.Expr; +const ExprNodeList = js_ast.ExprNodeList; +const G = js_ast.G; +const Macro = js_ast.Macro; +const S = js_ast.S; +const Stmt = js_ast.Stmt; +const ToJSError = js_ast.ToJSError; diff --git a/src/ast/NewStore.zig b/src/ast/NewStore.zig new file mode 100644 index 0000000000..b24640ec43 --- /dev/null +++ b/src/ast/NewStore.zig @@ -0,0 +1,167 @@ +/// This "Store" is a specialized memory allocation strategy very similar to an +/// arena, used for allocating expression and statement nodes during JavaScript +/// parsing and visiting. Allocations are grouped into large blocks, where each +/// block is treated as a fixed-buffer allocator. When a block runs out of +/// space, a new one is created; all blocks are joined as a linked list. +/// +/// Similarly to an arena, you can call .reset() to reset state, reusing memory +/// across operations. +pub fn NewStore(comptime types: []const type, comptime count: usize) type { + const largest_size, const largest_align = brk: { + var largest_size = 0; + var largest_align = 1; + for (types) |T| { + if (@sizeOf(T) == 0) { + @compileError("NewStore does not support 0 size type: " ++ @typeName(T)); + } + largest_size = @max(@sizeOf(T), largest_size); + largest_align = @max(@alignOf(T), largest_align); + } + break :brk .{ largest_size, largest_align }; + }; + + const backing_allocator = bun.default_allocator; + + const log = Output.scoped(.Store, true); + + return struct { + const Store = @This(); + + current: *Block, + debug_lock: std.debug.SafetyLock = .{}, + + pub const Block = struct { + pub const size = largest_size * count * 2; + pub const Size = std.math.IntFittingRange(0, size + largest_size); + + buffer: [size]u8 align(largest_align) = undefined, + bytes_used: Size = 0, + next: ?*Block = null, + + pub fn tryAlloc(block: *Block, comptime T: type) ?*T { + const start = std.mem.alignForward(usize, block.bytes_used, @alignOf(T)); + if (start + @sizeOf(T) > block.buffer.len) return null; + defer block.bytes_used = @intCast(start + @sizeOf(T)); + + // it's simpler to use @ptrCast, but as a sanity check, we also + // try to compute the slice. Zig will report an out of bounds + // panic if the null detection logic above is wrong + if (Environment.isDebug) { + _ = block.buffer[block.bytes_used..][0..@sizeOf(T)]; + } + + return @alignCast(@ptrCast(&block.buffer[start])); + } + }; + + const PreAlloc = struct { + metadata: Store, + first_block: Block, + }; + + pub fn firstBlock(store: *Store) *Block { + return &@as(*PreAlloc, @fieldParentPtr("metadata", store)).first_block; + } + + pub fn init() *Store { + log("init", .{}); + const prealloc = backing_allocator.create(PreAlloc) catch bun.outOfMemory(); + + prealloc.first_block.bytes_used = 0; + prealloc.first_block.next = null; + + prealloc.metadata = .{ + .current = &prealloc.first_block, + }; + + return &prealloc.metadata; + } + + pub fn deinit(store: *Store) void { + log("deinit", .{}); + var it = store.firstBlock().next; // do not free `store.head` + while (it) |next| { + if (Environment.isDebug or Environment.enable_asan) + @memset(next.buffer, undefined); + it = next.next; + backing_allocator.destroy(next); + } + + const prealloc: PreAlloc = @fieldParentPtr("metadata", store); + bun.assert(&prealloc.first_block == store.head); + backing_allocator.destroy(prealloc); + } + + pub fn reset(store: *Store) void { + log("reset", .{}); + + if (Environment.isDebug or Environment.enable_asan) { + var it: ?*Block = store.firstBlock(); + while (it) |next| : (it = next.next) { + next.bytes_used = undefined; + @memset(&next.buffer, undefined); + } + } + + store.current = store.firstBlock(); + store.current.bytes_used = 0; + } + + fn allocate(store: *Store, comptime T: type) *T { + comptime bun.assert(@sizeOf(T) > 0); // don't allocate! + comptime if (!supportsType(T)) { + @compileError("Store does not know about type: " ++ @typeName(T)); + }; + + if (store.current.tryAlloc(T)) |ptr| + return ptr; + + // a new block is needed + const next_block = if (store.current.next) |next| brk: { + next.bytes_used = 0; + break :brk next; + } else brk: { + const new_block = backing_allocator.create(Block) catch + bun.outOfMemory(); + new_block.next = null; + new_block.bytes_used = 0; + store.current.next = new_block; + break :brk new_block; + }; + + store.current = next_block; + + return next_block.tryAlloc(T) orelse + unreachable; // newly initialized blocks must have enough space for at least one + } + + pub inline fn append(store: *Store, comptime T: type, data: T) *T { + const ptr = store.allocate(T); + if (Environment.isDebug) { + log("append({s}) -> 0x{x}", .{ bun.meta.typeName(T), @intFromPtr(ptr) }); + } + ptr.* = data; + return ptr; + } + + pub fn lock(store: *Store) void { + store.debug_lock.lock(); + } + + pub fn unlock(store: *Store) void { + store.debug_lock.unlock(); + } + + fn supportsType(T: type) bool { + return std.mem.indexOfScalar(type, types, T) != null; + } + }; +} + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const Environment = bun.Environment; +const Output = bun.Output; diff --git a/src/ast/Op.zig b/src/ast/Op.zig new file mode 100644 index 0000000000..13c59b47f8 --- /dev/null +++ b/src/ast/Op.zig @@ -0,0 +1,293 @@ +// If you add a new token, remember to add it to "Table" too +pub const Code = enum { + // Prefix + un_pos, // +expr + un_neg, // -expr + un_cpl, // ~expr + un_not, // !expr + un_void, + un_typeof, + un_delete, + + // Prefix update + un_pre_dec, + un_pre_inc, + + // Postfix update + un_post_dec, + un_post_inc, + + /// Left-associative + bin_add, + /// Left-associative + bin_sub, + /// Left-associative + bin_mul, + /// Left-associative + bin_div, + /// Left-associative + bin_rem, + /// Left-associative + bin_pow, + /// Left-associative + bin_lt, + /// Left-associative + bin_le, + /// Left-associative + bin_gt, + /// Left-associative + bin_ge, + /// Left-associative + bin_in, + /// Left-associative + bin_instanceof, + /// Left-associative + bin_shl, + /// Left-associative + bin_shr, + /// Left-associative + bin_u_shr, + /// Left-associative + bin_loose_eq, + /// Left-associative + bin_loose_ne, + /// Left-associative + bin_strict_eq, + /// Left-associative + bin_strict_ne, + /// Left-associative + bin_nullish_coalescing, + /// Left-associative + bin_logical_or, + /// Left-associative + bin_logical_and, + /// Left-associative + bin_bitwise_or, + /// Left-associative + bin_bitwise_and, + /// Left-associative + bin_bitwise_xor, + + /// Non-associative + bin_comma, + + /// Right-associative + bin_assign, + /// Right-associative + bin_add_assign, + /// Right-associative + bin_sub_assign, + /// Right-associative + bin_mul_assign, + /// Right-associative + bin_div_assign, + /// Right-associative + bin_rem_assign, + /// Right-associative + bin_pow_assign, + /// Right-associative + bin_shl_assign, + /// Right-associative + bin_shr_assign, + /// Right-associative + bin_u_shr_assign, + /// Right-associative + bin_bitwise_or_assign, + /// Right-associative + bin_bitwise_and_assign, + /// Right-associative + bin_bitwise_xor_assign, + /// Right-associative + bin_nullish_coalescing_assign, + /// Right-associative + bin_logical_or_assign, + /// Right-associative + bin_logical_and_assign, + + pub fn jsonStringify(self: @This(), writer: anytype) !void { + return try writer.write(@tagName(self)); + } + + pub fn unaryAssignTarget(code: Op.Code) AssignTarget { + if (@intFromEnum(code) >= + @intFromEnum(Op.Code.un_pre_dec) and @intFromEnum(code) <= + @intFromEnum(Op.Code.un_post_inc)) + { + return AssignTarget.update; + } + + return AssignTarget.none; + } + pub fn isLeftAssociative(code: Op.Code) bool { + return @intFromEnum(code) >= + @intFromEnum(Op.Code.bin_add) and + @intFromEnum(code) < @intFromEnum(Op.Code.bin_comma) and code != .bin_pow; + } + pub fn isRightAssociative(code: Op.Code) bool { + return @intFromEnum(code) >= @intFromEnum(Op.Code.bin_assign) or code == .bin_pow; + } + pub fn binaryAssignTarget(code: Op.Code) AssignTarget { + if (code == .bin_assign) { + return AssignTarget.replace; + } + + if (@intFromEnum(code) > @intFromEnum(Op.Code.bin_assign)) { + return AssignTarget.update; + } + + return AssignTarget.none; + } + + pub fn isPrefix(code: Op.Code) bool { + return @intFromEnum(code) < @intFromEnum(Op.Code.un_post_dec); + } +}; + +pub const Level = enum(u6) { + lowest, + comma, + spread, + yield, + assign, + conditional, + nullish_coalescing, + logical_or, + logical_and, + bitwise_or, + bitwise_xor, + bitwise_and, + equals, + compare, + shift, + add, + multiply, + exponentiation, + prefix, + postfix, + new, + call, + member, + + pub inline fn lt(self: Level, b: Level) bool { + return @intFromEnum(self) < @intFromEnum(b); + } + pub inline fn gt(self: Level, b: Level) bool { + return @intFromEnum(self) > @intFromEnum(b); + } + pub inline fn gte(self: Level, b: Level) bool { + return @intFromEnum(self) >= @intFromEnum(b); + } + pub inline fn lte(self: Level, b: Level) bool { + return @intFromEnum(self) <= @intFromEnum(b); + } + pub inline fn eql(self: Level, b: Level) bool { + return @intFromEnum(self) == @intFromEnum(b); + } + + pub inline fn sub(self: Level, i: anytype) Level { + return @as(Level, @enumFromInt(@intFromEnum(self) - i)); + } + + pub inline fn addF(self: Level, i: anytype) Level { + return @as(Level, @enumFromInt(@intFromEnum(self) + i)); + } +}; + +text: string, +level: Level, +is_keyword: bool = false, + +pub fn init(triple: anytype) Op { + return Op{ + .text = triple.@"0", + .level = triple.@"1", + .is_keyword = triple.@"2", + }; +} + +pub fn jsonStringify(self: *const @This(), writer: anytype) !void { + return try writer.write(self.text); +} + +pub const TableType: std.EnumArray(Op.Code, Op) = undefined; +pub const Table = brk: { + var table = std.EnumArray(Op.Code, Op).initUndefined(); + + // Prefix + table.set(Op.Code.un_pos, Op.init(.{ "+", Level.prefix, false })); + table.set(Op.Code.un_neg, Op.init(.{ "-", Level.prefix, false })); + table.set(Op.Code.un_cpl, Op.init(.{ "~", Level.prefix, false })); + table.set(Op.Code.un_not, Op.init(.{ "!", Level.prefix, false })); + table.set(Op.Code.un_void, Op.init(.{ "void", Level.prefix, true })); + table.set(Op.Code.un_typeof, Op.init(.{ "typeof", Level.prefix, true })); + table.set(Op.Code.un_delete, Op.init(.{ "delete", Level.prefix, true })); + + // Prefix update + table.set(Op.Code.un_pre_dec, Op.init(.{ "--", Level.prefix, false })); + table.set(Op.Code.un_pre_inc, Op.init(.{ "++", Level.prefix, false })); + + // Postfix update + table.set(Op.Code.un_post_dec, Op.init(.{ "--", Level.postfix, false })); + table.set(Op.Code.un_post_inc, Op.init(.{ "++", Level.postfix, false })); + + // Left-associative + table.set(Op.Code.bin_add, Op.init(.{ "+", Level.add, false })); + table.set(Op.Code.bin_sub, Op.init(.{ "-", Level.add, false })); + table.set(Op.Code.bin_mul, Op.init(.{ "*", Level.multiply, false })); + table.set(Op.Code.bin_div, Op.init(.{ "/", Level.multiply, false })); + table.set(Op.Code.bin_rem, Op.init(.{ "%", Level.multiply, false })); + table.set(Op.Code.bin_pow, Op.init(.{ "**", Level.exponentiation, false })); + table.set(Op.Code.bin_lt, Op.init(.{ "<", Level.compare, false })); + table.set(Op.Code.bin_le, Op.init(.{ "<=", Level.compare, false })); + table.set(Op.Code.bin_gt, Op.init(.{ ">", Level.compare, false })); + table.set(Op.Code.bin_ge, Op.init(.{ ">=", Level.compare, false })); + table.set(Op.Code.bin_in, Op.init(.{ "in", Level.compare, true })); + table.set(Op.Code.bin_instanceof, Op.init(.{ "instanceof", Level.compare, true })); + table.set(Op.Code.bin_shl, Op.init(.{ "<<", Level.shift, false })); + table.set(Op.Code.bin_shr, Op.init(.{ ">>", Level.shift, false })); + table.set(Op.Code.bin_u_shr, Op.init(.{ ">>>", Level.shift, false })); + table.set(Op.Code.bin_loose_eq, Op.init(.{ "==", Level.equals, false })); + table.set(Op.Code.bin_loose_ne, Op.init(.{ "!=", Level.equals, false })); + table.set(Op.Code.bin_strict_eq, Op.init(.{ "===", Level.equals, false })); + table.set(Op.Code.bin_strict_ne, Op.init(.{ "!==", Level.equals, false })); + table.set(Op.Code.bin_nullish_coalescing, Op.init(.{ "??", Level.nullish_coalescing, false })); + table.set(Op.Code.bin_logical_or, Op.init(.{ "||", Level.logical_or, false })); + table.set(Op.Code.bin_logical_and, Op.init(.{ "&&", Level.logical_and, false })); + table.set(Op.Code.bin_bitwise_or, Op.init(.{ "|", Level.bitwise_or, false })); + table.set(Op.Code.bin_bitwise_and, Op.init(.{ "&", Level.bitwise_and, false })); + table.set(Op.Code.bin_bitwise_xor, Op.init(.{ "^", Level.bitwise_xor, false })); + + // Non-associative + table.set(Op.Code.bin_comma, Op.init(.{ ",", Level.comma, false })); + + // Right-associative + table.set(Op.Code.bin_assign, Op.init(.{ "=", Level.assign, false })); + table.set(Op.Code.bin_add_assign, Op.init(.{ "+=", Level.assign, false })); + table.set(Op.Code.bin_sub_assign, Op.init(.{ "-=", Level.assign, false })); + table.set(Op.Code.bin_mul_assign, Op.init(.{ "*=", Level.assign, false })); + table.set(Op.Code.bin_div_assign, Op.init(.{ "/=", Level.assign, false })); + table.set(Op.Code.bin_rem_assign, Op.init(.{ "%=", Level.assign, false })); + table.set(Op.Code.bin_pow_assign, Op.init(.{ "**=", Level.assign, false })); + table.set(Op.Code.bin_shl_assign, Op.init(.{ "<<=", Level.assign, false })); + table.set(Op.Code.bin_shr_assign, Op.init(.{ ">>=", Level.assign, false })); + table.set(Op.Code.bin_u_shr_assign, Op.init(.{ ">>>=", Level.assign, false })); + table.set(Op.Code.bin_bitwise_or_assign, Op.init(.{ "|=", Level.assign, false })); + table.set(Op.Code.bin_bitwise_and_assign, Op.init(.{ "&=", Level.assign, false })); + table.set(Op.Code.bin_bitwise_xor_assign, Op.init(.{ "^=", Level.assign, false })); + table.set(Op.Code.bin_nullish_coalescing_assign, Op.init(.{ "??=", Level.assign, false })); + table.set(Op.Code.bin_logical_or_assign, Op.init(.{ "||=", Level.assign, false })); + table.set(Op.Code.bin_logical_and_assign, Op.init(.{ "&&=", Level.assign, false })); + + break :brk table; +}; + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const string = bun.string; + +const js_ast = bun.js_ast; +const AssignTarget = js_ast.AssignTarget; +const Op = js_ast.Op; diff --git a/src/ast/S.zig b/src/ast/S.zig new file mode 100644 index 0000000000..e47c3f223a --- /dev/null +++ b/src/ast/S.zig @@ -0,0 +1,233 @@ +pub const Block = struct { + stmts: StmtNodeList, + close_brace_loc: logger.Loc = logger.Loc.Empty, +}; + +pub const SExpr = struct { + value: ExprNodeIndex, + + // This is set to true for automatically-generated expressions that should + // not affect tree shaking. For example, calling a function from the runtime + // that doesn't have externally-visible side effects. + does_not_affect_tree_shaking: bool = false, +}; + +pub const Comment = struct { text: string }; + +pub const Directive = struct { + value: []const u8, +}; + +pub const ExportClause = struct { + items: []ClauseItem, + is_single_line: bool, +}; + +pub const Empty = struct {}; + +pub const ExportStar = struct { + namespace_ref: Ref, + alias: ?G.ExportStarAlias = null, + import_record_index: u32, +}; + +// This is an "export = value;" statement in TypeScript +pub const ExportEquals = struct { value: ExprNodeIndex }; + +pub const Label = struct { name: LocRef, stmt: StmtNodeIndex }; + +// This is a stand-in for a TypeScript type declaration +pub const TypeScript = struct {}; + +pub const Debugger = struct {}; + +pub const ExportFrom = struct { + items: []ClauseItem, + namespace_ref: Ref, + import_record_index: u32, + is_single_line: bool, +}; + +pub const ExportDefault = struct { + default_name: LocRef, // value may be a SFunction or SClass + value: StmtOrExpr, + + pub fn canBeMoved(self: *const ExportDefault) bool { + return switch (self.value) { + .expr => |e| e.canBeMoved(), + .stmt => |s| switch (s.data) { + .s_class => |class| class.class.canBeMoved(), + .s_function => true, + else => false, + }, + }; + } +}; + +pub const Enum = struct { + name: LocRef, + arg: Ref, + values: []EnumValue, + is_export: bool, +}; + +pub const Namespace = struct { + name: LocRef, + arg: Ref, + stmts: StmtNodeList, + is_export: bool, +}; + +pub const Function = struct { + func: G.Fn, +}; + +pub const Class = struct { class: G.Class, is_export: bool = false }; + +pub const If = struct { + test_: ExprNodeIndex, + yes: StmtNodeIndex, + no: ?StmtNodeIndex, +}; + +pub const For = struct { + // May be a SConst, SLet, SVar, or SExpr + init: ?StmtNodeIndex = null, + test_: ?ExprNodeIndex = null, + update: ?ExprNodeIndex = null, + body: StmtNodeIndex, +}; + +pub const ForIn = struct { + // May be a SConst, SLet, SVar, or SExpr + init: StmtNodeIndex, + value: ExprNodeIndex, + body: StmtNodeIndex, +}; + +pub const ForOf = struct { + is_await: bool = false, + // May be a SConst, SLet, SVar, or SExpr + init: StmtNodeIndex, + value: ExprNodeIndex, + body: StmtNodeIndex, +}; + +pub const DoWhile = struct { body: StmtNodeIndex, test_: ExprNodeIndex }; + +pub const While = struct { + test_: ExprNodeIndex, + body: StmtNodeIndex, +}; + +pub const With = struct { + value: ExprNodeIndex, + body: StmtNodeIndex, + body_loc: logger.Loc = logger.Loc.Empty, +}; + +pub const Try = struct { + body_loc: logger.Loc, + body: StmtNodeList, + + catch_: ?Catch = null, + finally: ?Finally = null, +}; + +pub const Switch = struct { + test_: ExprNodeIndex, + body_loc: logger.Loc, + cases: []Case, +}; + +// This object represents all of these types of import statements: +// +// import 'path' +// import {item1, item2} from 'path' +// import * as ns from 'path' +// import defaultItem, {item1, item2} from 'path' +// import defaultItem, * as ns from 'path' +// +// Many parts are optional and can be combined in different ways. The only +// restriction is that you cannot have both a clause and a star namespace. +pub const Import = struct { + // If this is a star import: This is a Ref for the namespace symbol. The Loc + // for the symbol is StarLoc. + // + // Otherwise: This is an auto-generated Ref for the namespace representing + // the imported file. In this case StarLoc is nil. The NamespaceRef is used + // when converting this module to a CommonJS module. + namespace_ref: Ref, + default_name: ?LocRef = null, + items: []ClauseItem = &.{}, + star_name_loc: ?logger.Loc = null, + import_record_index: u32, + is_single_line: bool = false, +}; + +pub const Return = struct { value: ?ExprNodeIndex = null }; +pub const Throw = struct { value: ExprNodeIndex }; + +pub const Local = struct { + kind: Kind = .k_var, + decls: G.Decl.List = .{}, + is_export: bool = false, + // The TypeScript compiler doesn't generate code for "import foo = bar" + // statements where the import is never used. + was_ts_import_equals: bool = false, + + was_commonjs_export: bool = false, + + pub fn canMergeWith(this: *const Local, other: *const Local) bool { + return this.kind == other.kind and this.is_export == other.is_export and + this.was_commonjs_export == other.was_commonjs_export; + } + + pub const Kind = enum { + k_var, + k_let, + k_const, + k_using, + k_await_using, + + pub fn jsonStringify(self: @This(), writer: anytype) !void { + return try writer.write(@tagName(self)); + } + + pub fn isUsing(self: Kind) bool { + return self == .k_using or self == .k_await_using; + } + + pub fn isReassignable(kind: Kind) bool { + return kind == .k_var or kind == .k_let; + } + }; +}; + +pub const Break = struct { + label: ?LocRef = null, +}; + +pub const Continue = struct { + label: ?LocRef = null, +}; + +// @sortImports + +const bun = @import("bun"); +const logger = bun.logger; +const string = bun.string; + +const js_ast = bun.js_ast; +const Case = js_ast.Case; +const Catch = js_ast.Catch; +const ClauseItem = js_ast.ClauseItem; +const EnumValue = js_ast.EnumValue; +const ExprNodeIndex = js_ast.ExprNodeIndex; +const Finally = js_ast.Finally; +const G = js_ast.G; +const LocRef = js_ast.LocRef; +const Ref = js_ast.Ref; +const StmtNodeIndex = js_ast.StmtNodeIndex; +const StmtNodeList = js_ast.StmtNodeList; +const StmtOrExpr = js_ast.StmtOrExpr; diff --git a/src/ast/Scope.zig b/src/ast/Scope.zig new file mode 100644 index 0000000000..ca9334e0d2 --- /dev/null +++ b/src/ast/Scope.zig @@ -0,0 +1,222 @@ +pub const MemberHashMap = bun.StringHashMapUnmanaged(Member); + +id: usize = 0, +kind: Kind = Kind.block, +parent: ?*Scope = null, +children: BabyList(*Scope) = .{}, +members: MemberHashMap = .{}, +generated: BabyList(Ref) = .{}, + +// This is used to store the ref of the label symbol for ScopeLabel scopes. +label_ref: ?Ref = null, +label_stmt_is_loop: bool = false, + +// If a scope contains a direct eval() expression, then none of the symbols +// inside that scope can be renamed. We conservatively assume that the +// evaluated code might reference anything that it has access to. +contains_direct_eval: bool = false, + +// This is to help forbid "arguments" inside class body scopes +forbid_arguments: bool = false, + +strict_mode: StrictModeKind = StrictModeKind.sloppy_mode, + +is_after_const_local_prefix: bool = false, + +// This will be non-null if this is a TypeScript "namespace" or "enum" +ts_namespace: ?*TSNamespaceScope = null, + +pub const NestedScopeMap = std.AutoArrayHashMap(u32, bun.BabyList(*Scope)); + +pub fn getMemberHash(name: []const u8) u64 { + return bun.StringHashMapContext.hash(.{}, name); +} + +pub fn getMemberWithHash(this: *const Scope, name: []const u8, hash_value: u64) ?Member { + const hashed = bun.StringHashMapContext.Prehashed{ + .value = hash_value, + .input = name, + }; + return this.members.getAdapted(name, hashed); +} + +pub fn getOrPutMemberWithHash( + this: *Scope, + allocator: std.mem.Allocator, + name: []const u8, + hash_value: u64, +) !MemberHashMap.GetOrPutResult { + const hashed = bun.StringHashMapContext.Prehashed{ + .value = hash_value, + .input = name, + }; + return this.members.getOrPutContextAdapted(allocator, name, hashed, .{}); +} + +pub fn reset(this: *Scope) void { + this.children.clearRetainingCapacity(); + this.generated.clearRetainingCapacity(); + this.members.clearRetainingCapacity(); + this.parent = null; + this.id = 0; + this.label_ref = null; + this.label_stmt_is_loop = false; + this.contains_direct_eval = false; + this.strict_mode = .sloppy_mode; + this.kind = .block; +} + +// Do not make this a packed struct +// Two hours of debugging time lost to that. +// It causes a crash due to undefined memory +pub const Member = struct { + ref: Ref, + loc: logger.Loc, + + pub fn eql(a: Member, b: Member) bool { + return @call(bun.callmod_inline, Ref.eql, .{ a.ref, b.ref }) and a.loc.start == b.loc.start; + } +}; + +pub const SymbolMergeResult = enum { + forbidden, + replace_with_new, + overwrite_with_new, + keep_existing, + become_private_get_set_pair, + become_private_static_get_set_pair, +}; + +pub fn canMergeSymbols( + scope: *Scope, + existing: Symbol.Kind, + new: Symbol.Kind, + comptime is_typescript_enabled: bool, +) SymbolMergeResult { + if (existing == .unbound) { + return .replace_with_new; + } + + if (comptime is_typescript_enabled) { + // In TypeScript, imports are allowed to silently collide with symbols within + // the module. Presumably this is because the imports may be type-only: + // + // import {Foo} from 'bar' + // class Foo {} + // + if (existing == .import) { + return .replace_with_new; + } + + // "enum Foo {} enum Foo {}" + // "namespace Foo { ... } enum Foo {}" + if (new == .ts_enum and (existing == .ts_enum or existing == .ts_namespace)) { + return .replace_with_new; + } + + // "namespace Foo { ... } namespace Foo { ... }" + // "function Foo() {} namespace Foo { ... }" + // "enum Foo {} namespace Foo { ... }" + if (new == .ts_namespace) { + switch (existing) { + .ts_namespace, + .ts_enum, + .hoisted_function, + .generator_or_async_function, + .class, + => return .keep_existing, + else => {}, + } + } + } + + // "var foo; var foo;" + // "var foo; function foo() {}" + // "function foo() {} var foo;" + // "function *foo() {} function *foo() {}" but not "{ function *foo() {} function *foo() {} }" + if (Symbol.isKindHoistedOrFunction(new) and + Symbol.isKindHoistedOrFunction(existing) and + (scope.kind == .entry or scope.kind == .function_body or scope.kind == .function_args or + (new == existing and Symbol.isKindHoisted(existing)))) + { + return .replace_with_new; + } + + // "get #foo() {} set #foo() {}" + // "set #foo() {} get #foo() {}" + if ((existing == .private_get and new == .private_set) or + (existing == .private_set and new == .private_get)) + { + return .become_private_get_set_pair; + } + if ((existing == .private_static_get and new == .private_static_set) or + (existing == .private_static_set and new == .private_static_get)) + { + return .become_private_static_get_set_pair; + } + + // "try {} catch (e) { var e }" + if (existing == .catch_identifier and new == .hoisted) { + return .replace_with_new; + } + + // "function() { var arguments }" + if (existing == .arguments and new == .hoisted) { + return .keep_existing; + } + + // "function() { let arguments }" + if (existing == .arguments and new != .hoisted) { + return .overwrite_with_new; + } + + return .forbidden; +} + +pub const Kind = enum(u8) { + block, + with, + label, + class_name, + class_body, + catch_binding, + + // The scopes below stop hoisted variables from extending into parent scopes + entry, // This is a module, TypeScript enum, or TypeScript namespace + function_args, + function_body, + class_static_init, + + pub fn jsonStringify(self: @This(), writer: anytype) !void { + return try writer.write(@tagName(self)); + } +}; + +pub fn recursiveSetStrictMode(s: *Scope, kind: StrictModeKind) void { + if (s.strict_mode == .sloppy_mode) { + s.strict_mode = kind; + for (s.children.slice()) |child| { + child.recursiveSetStrictMode(kind); + } + } +} + +pub inline fn kindStopsHoisting(s: *const Scope) bool { + return @intFromEnum(s.kind) >= @intFromEnum(Kind.entry); +} + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const BabyList = bun.BabyList; +const logger = bun.logger; + +const js_ast = bun.js_ast; +const Ref = js_ast.Ref; +const Scope = js_ast.Scope; +const StrictModeKind = js_ast.StrictModeKind; +const Symbol = js_ast.Symbol; +const TSNamespaceScope = js_ast.TSNamespaceScope; +const TypeScript = js_ast.TypeScript; diff --git a/src/ast/ServerComponentBoundary.zig b/src/ast/ServerComponentBoundary.zig new file mode 100644 index 0000000000..b24af72401 --- /dev/null +++ b/src/ast/ServerComponentBoundary.zig @@ -0,0 +1,123 @@ +//! Represents a boundary between client and server code. Every boundary +//! gets bundled twice, once for the desired target, and once to generate +//! a module of "references". Specifically, the generated file takes the +//! canonical Ast as input to derive a wrapper. See `Framework.ServerComponents` +//! for more details about this generated file. +//! +//! This is sometimes abbreviated as SCB +use_directive: UseDirective, + +/// The index of the original file. +source_index: Index.Int, + +/// Index to the file imported on the opposite platform, which is +/// generated by the bundler. For client components, this is the +/// server's code. For server actions, this is the client's code. +reference_source_index: Index.Int, + +/// When `bake.Framework.ServerComponents.separate_ssr_graph` is enabled this +/// points to the separated module. When the SSR graph is not separate, this is +/// equal to `reference_source_index` +// +// TODO: Is this used for server actions. +ssr_source_index: Index.Int, + +/// The requirements for this data structure is to have reasonable lookup +/// speed, but also being able to pull a `[]const Index.Int` of all +/// boundaries for iteration. +pub const List = struct { + list: std.MultiArrayList(ServerComponentBoundary) = .{}, + /// Used to facilitate fast lookups into `items` by `.source_index` + map: Map = .{}, + + const Map = std.ArrayHashMapUnmanaged(void, void, struct {}, true); + + /// Can only be called on the bundler thread. + pub fn put( + m: *List, + allocator: std.mem.Allocator, + source_index: Index.Int, + use_directive: UseDirective, + reference_source_index: Index.Int, + ssr_source_index: Index.Int, + ) !void { + try m.list.append(allocator, .{ + .source_index = source_index, + .use_directive = use_directive, + .reference_source_index = reference_source_index, + .ssr_source_index = ssr_source_index, + }); + const gop = try m.map.getOrPutAdapted( + allocator, + source_index, + Adapter{ .list = m.list.slice() }, + ); + bun.assert(!gop.found_existing); + } + + /// Can only be called on the bundler thread. + pub fn getIndex(l: *const List, real_source_index: Index.Int) ?usize { + return l.map.getIndexAdapted( + real_source_index, + Adapter{ .list = l.list.slice() }, + ); + } + + /// Use this to improve speed of accessing fields at the cost of + /// storing more pointers. Invalidated when input is mutated. + pub fn slice(l: List) Slice { + return .{ .list = l.list.slice(), .map = l.map }; + } + + pub const Slice = struct { + list: std.MultiArrayList(ServerComponentBoundary).Slice, + map: Map, + + pub fn getIndex(l: *const Slice, real_source_index: Index.Int) ?usize { + return l.map.getIndexAdapted( + real_source_index, + Adapter{ .list = l.list }, + ) orelse return null; + } + + pub fn getReferenceSourceIndex(l: *const Slice, real_source_index: Index.Int) ?u32 { + const i = l.map.getIndexAdapted( + real_source_index, + Adapter{ .list = l.list }, + ) orelse return null; + bun.unsafeAssert(l.list.capacity > 0); // optimize MultiArrayList.Slice.items + return l.list.items(.reference_source_index)[i]; + } + + pub fn bitSet(scbs: Slice, alloc: std.mem.Allocator, input_file_count: usize) !bun.bit_set.DynamicBitSetUnmanaged { + var scb_bitset = try bun.bit_set.DynamicBitSetUnmanaged.initEmpty(alloc, input_file_count); + for (scbs.list.items(.source_index)) |source_index| { + scb_bitset.set(source_index); + } + return scb_bitset; + } + }; + + pub const Adapter = struct { + list: std.MultiArrayList(ServerComponentBoundary).Slice, + + pub fn hash(_: Adapter, key: Index.Int) u32 { + return std.hash.uint32(key); + } + + pub fn eql(adapt: Adapter, a: Index.Int, _: void, b_index: usize) bool { + bun.unsafeAssert(adapt.list.capacity > 0); // optimize MultiArrayList.Slice.items + return a == adapt.list.items(.source_index)[b_index]; + } + }; +}; + +// @sortImports + +const bun = @import("bun"); +const std = @import("std"); + +const js_ast = bun.js_ast; +const Index = js_ast.Index; +const ServerComponentBoundary = js_ast.ServerComponentBoundary; +const UseDirective = js_ast.UseDirective; diff --git a/src/ast/Stmt.zig b/src/ast/Stmt.zig new file mode 100644 index 0000000000..90cf71d27a --- /dev/null +++ b/src/ast/Stmt.zig @@ -0,0 +1,424 @@ +loc: logger.Loc, +data: Data, + +pub const Batcher = NewBatcher(Stmt); + +pub fn assign(a: Expr, b: Expr) Stmt { + return Stmt.alloc( + S.SExpr, + S.SExpr{ + .value = Expr.assign(a, b), + }, + a.loc, + ); +} + +const Serializable = struct { + type: Tag, + object: string, + value: Data, + loc: logger.Loc, +}; + +pub fn jsonStringify(self: *const Stmt, writer: anytype) !void { + return try writer.write(Serializable{ .type = std.meta.activeTag(self.data), .object = "stmt", .value = self.data, .loc = self.loc }); +} + +pub fn isTypeScript(self: *Stmt) bool { + return @as(Stmt.Tag, self.data) == .s_type_script; +} + +pub fn isSuperCall(self: Stmt) bool { + return self.data == .s_expr and self.data.s_expr.value.data == .e_call and self.data.s_expr.value.data.e_call.target.data == .e_super; +} + +pub fn isMissingExpr(self: Stmt) bool { + return self.data == .s_expr and self.data.s_expr.value.data == .e_missing; +} + +pub fn empty() Stmt { + return Stmt{ .data = .{ .s_empty = None }, .loc = logger.Loc{} }; +} + +pub fn toEmpty(this: Stmt) Stmt { + return .{ + .data = .{ + .s_empty = None, + }, + .loc = this.loc, + }; +} + +const None = S.Empty{}; + +pub var icount: usize = 0; +pub fn init(comptime StatementType: type, origData: *StatementType, loc: logger.Loc) Stmt { + icount += 1; + + return switch (comptime StatementType) { + S.Empty => Stmt{ .loc = loc, .data = Data{ .s_empty = S.Empty{} } }, + S.Block => Stmt.comptime_init("s_block", S.Block, origData, loc), + S.Break => Stmt.comptime_init("s_break", S.Break, origData, loc), + S.Class => Stmt.comptime_init("s_class", S.Class, origData, loc), + S.Comment => Stmt.comptime_init("s_comment", S.Comment, origData, loc), + S.Continue => Stmt.comptime_init("s_continue", S.Continue, origData, loc), + S.Debugger => Stmt.comptime_init("s_debugger", S.Debugger, origData, loc), + S.Directive => Stmt.comptime_init("s_directive", S.Directive, origData, loc), + S.DoWhile => Stmt.comptime_init("s_do_while", S.DoWhile, origData, loc), + S.Enum => Stmt.comptime_init("s_enum", S.Enum, origData, loc), + S.ExportClause => Stmt.comptime_init("s_export_clause", S.ExportClause, origData, loc), + S.ExportDefault => Stmt.comptime_init("s_export_default", S.ExportDefault, origData, loc), + S.ExportEquals => Stmt.comptime_init("s_export_equals", S.ExportEquals, origData, loc), + S.ExportFrom => Stmt.comptime_init("s_export_from", S.ExportFrom, origData, loc), + S.ExportStar => Stmt.comptime_init("s_export_star", S.ExportStar, origData, loc), + S.SExpr => Stmt.comptime_init("s_expr", S.SExpr, origData, loc), + S.ForIn => Stmt.comptime_init("s_for_in", S.ForIn, origData, loc), + S.ForOf => Stmt.comptime_init("s_for_of", S.ForOf, origData, loc), + S.For => Stmt.comptime_init("s_for", S.For, origData, loc), + S.Function => Stmt.comptime_init("s_function", S.Function, origData, loc), + S.If => Stmt.comptime_init("s_if", S.If, origData, loc), + S.Import => Stmt.comptime_init("s_import", S.Import, origData, loc), + S.Label => Stmt.comptime_init("s_label", S.Label, origData, loc), + S.Local => Stmt.comptime_init("s_local", S.Local, origData, loc), + S.Namespace => Stmt.comptime_init("s_namespace", S.Namespace, origData, loc), + S.Return => Stmt.comptime_init("s_return", S.Return, origData, loc), + S.Switch => Stmt.comptime_init("s_switch", S.Switch, origData, loc), + S.Throw => Stmt.comptime_init("s_throw", S.Throw, origData, loc), + S.Try => Stmt.comptime_init("s_try", S.Try, origData, loc), + S.TypeScript => Stmt.comptime_init("s_type_script", S.TypeScript, origData, loc), + S.While => Stmt.comptime_init("s_while", S.While, origData, loc), + S.With => Stmt.comptime_init("s_with", S.With, origData, loc), + else => @compileError("Invalid type in Stmt.init"), + }; +} +inline fn comptime_alloc(comptime tag_name: string, comptime typename: type, origData: anytype, loc: logger.Loc) Stmt { + return Stmt{ + .loc = loc, + .data = @unionInit( + Data, + tag_name, + Data.Store.append( + typename, + origData, + ), + ), + }; +} + +fn allocateData(allocator: std.mem.Allocator, comptime tag_name: string, comptime typename: type, origData: anytype, loc: logger.Loc) Stmt { + const value = allocator.create(@TypeOf(origData)) catch unreachable; + value.* = origData; + + return comptime_init(tag_name, *typename, value, loc); +} + +inline fn comptime_init(comptime tag_name: string, comptime TypeName: type, origData: TypeName, loc: logger.Loc) Stmt { + return Stmt{ .loc = loc, .data = @unionInit(Data, tag_name, origData) }; +} + +pub fn alloc(comptime StatementData: type, origData: StatementData, loc: logger.Loc) Stmt { + Stmt.Data.Store.assert(); + + icount += 1; + return switch (StatementData) { + S.Block => Stmt.comptime_alloc("s_block", S.Block, origData, loc), + S.Break => Stmt.comptime_alloc("s_break", S.Break, origData, loc), + S.Class => Stmt.comptime_alloc("s_class", S.Class, origData, loc), + S.Comment => Stmt.comptime_alloc("s_comment", S.Comment, origData, loc), + S.Continue => Stmt.comptime_alloc("s_continue", S.Continue, origData, loc), + S.Debugger => Stmt{ .loc = loc, .data = .{ .s_debugger = origData } }, + S.Directive => Stmt.comptime_alloc("s_directive", S.Directive, origData, loc), + S.DoWhile => Stmt.comptime_alloc("s_do_while", S.DoWhile, origData, loc), + S.Empty => Stmt{ .loc = loc, .data = Data{ .s_empty = S.Empty{} } }, + S.Enum => Stmt.comptime_alloc("s_enum", S.Enum, origData, loc), + S.ExportClause => Stmt.comptime_alloc("s_export_clause", S.ExportClause, origData, loc), + S.ExportDefault => Stmt.comptime_alloc("s_export_default", S.ExportDefault, origData, loc), + S.ExportEquals => Stmt.comptime_alloc("s_export_equals", S.ExportEquals, origData, loc), + S.ExportFrom => Stmt.comptime_alloc("s_export_from", S.ExportFrom, origData, loc), + S.ExportStar => Stmt.comptime_alloc("s_export_star", S.ExportStar, origData, loc), + S.SExpr => Stmt.comptime_alloc("s_expr", S.SExpr, origData, loc), + S.ForIn => Stmt.comptime_alloc("s_for_in", S.ForIn, origData, loc), + S.ForOf => Stmt.comptime_alloc("s_for_of", S.ForOf, origData, loc), + S.For => Stmt.comptime_alloc("s_for", S.For, origData, loc), + S.Function => Stmt.comptime_alloc("s_function", S.Function, origData, loc), + S.If => Stmt.comptime_alloc("s_if", S.If, origData, loc), + S.Import => Stmt.comptime_alloc("s_import", S.Import, origData, loc), + S.Label => Stmt.comptime_alloc("s_label", S.Label, origData, loc), + S.Local => Stmt.comptime_alloc("s_local", S.Local, origData, loc), + S.Namespace => Stmt.comptime_alloc("s_namespace", S.Namespace, origData, loc), + S.Return => Stmt.comptime_alloc("s_return", S.Return, origData, loc), + S.Switch => Stmt.comptime_alloc("s_switch", S.Switch, origData, loc), + S.Throw => Stmt.comptime_alloc("s_throw", S.Throw, origData, loc), + S.Try => Stmt.comptime_alloc("s_try", S.Try, origData, loc), + S.TypeScript => Stmt{ .loc = loc, .data = Data{ .s_type_script = S.TypeScript{} } }, + S.While => Stmt.comptime_alloc("s_while", S.While, origData, loc), + S.With => Stmt.comptime_alloc("s_with", S.With, origData, loc), + else => @compileError("Invalid type in Stmt.init"), + }; +} + +pub const Disabler = bun.DebugOnlyDisabler(@This()); + +/// When the lifetime of an Stmt.Data's pointer must exist longer than reset() is called, use this function. +/// Be careful to free the memory (or use an allocator that does it for you) +/// Also, prefer Stmt.init or Stmt.alloc when possible. This will be slower. +pub fn allocate(allocator: std.mem.Allocator, comptime StatementData: type, origData: StatementData, loc: logger.Loc) Stmt { + Stmt.Data.Store.assert(); + + icount += 1; + return switch (StatementData) { + S.Block => Stmt.allocateData(allocator, "s_block", S.Block, origData, loc), + S.Break => Stmt.allocateData(allocator, "s_break", S.Break, origData, loc), + S.Class => Stmt.allocateData(allocator, "s_class", S.Class, origData, loc), + S.Comment => Stmt.allocateData(allocator, "s_comment", S.Comment, origData, loc), + S.Continue => Stmt.allocateData(allocator, "s_continue", S.Continue, origData, loc), + S.Debugger => Stmt{ .loc = loc, .data = .{ .s_debugger = origData } }, + S.Directive => Stmt.allocateData(allocator, "s_directive", S.Directive, origData, loc), + S.DoWhile => Stmt.allocateData(allocator, "s_do_while", S.DoWhile, origData, loc), + S.Empty => Stmt{ .loc = loc, .data = Data{ .s_empty = S.Empty{} } }, + S.Enum => Stmt.allocateData(allocator, "s_enum", S.Enum, origData, loc), + S.ExportClause => Stmt.allocateData(allocator, "s_export_clause", S.ExportClause, origData, loc), + S.ExportDefault => Stmt.allocateData(allocator, "s_export_default", S.ExportDefault, origData, loc), + S.ExportEquals => Stmt.allocateData(allocator, "s_export_equals", S.ExportEquals, origData, loc), + S.ExportFrom => Stmt.allocateData(allocator, "s_export_from", S.ExportFrom, origData, loc), + S.ExportStar => Stmt.allocateData(allocator, "s_export_star", S.ExportStar, origData, loc), + S.SExpr => Stmt.allocateData(allocator, "s_expr", S.SExpr, origData, loc), + S.ForIn => Stmt.allocateData(allocator, "s_for_in", S.ForIn, origData, loc), + S.ForOf => Stmt.allocateData(allocator, "s_for_of", S.ForOf, origData, loc), + S.For => Stmt.allocateData(allocator, "s_for", S.For, origData, loc), + S.Function => Stmt.allocateData(allocator, "s_function", S.Function, origData, loc), + S.If => Stmt.allocateData(allocator, "s_if", S.If, origData, loc), + S.Import => Stmt.allocateData(allocator, "s_import", S.Import, origData, loc), + S.Label => Stmt.allocateData(allocator, "s_label", S.Label, origData, loc), + S.Local => Stmt.allocateData(allocator, "s_local", S.Local, origData, loc), + S.Namespace => Stmt.allocateData(allocator, "s_namespace", S.Namespace, origData, loc), + S.Return => Stmt.allocateData(allocator, "s_return", S.Return, origData, loc), + S.Switch => Stmt.allocateData(allocator, "s_switch", S.Switch, origData, loc), + S.Throw => Stmt.allocateData(allocator, "s_throw", S.Throw, origData, loc), + S.Try => Stmt.allocateData(allocator, "s_try", S.Try, origData, loc), + S.TypeScript => Stmt{ .loc = loc, .data = Data{ .s_type_script = S.TypeScript{} } }, + S.While => Stmt.allocateData(allocator, "s_while", S.While, origData, loc), + S.With => Stmt.allocateData(allocator, "s_with", S.With, origData, loc), + else => @compileError("Invalid type in Stmt.init"), + }; +} + +pub fn allocateExpr(allocator: std.mem.Allocator, expr: Expr) Stmt { + return Stmt.allocate(allocator, S.SExpr, S.SExpr{ .value = expr }, expr.loc); +} + +pub const Tag = enum { + s_block, + s_break, + s_class, + s_comment, + s_continue, + s_directive, + s_do_while, + s_enum, + s_export_clause, + s_export_default, + s_export_equals, + s_export_from, + s_export_star, + s_expr, + s_for_in, + s_for_of, + s_for, + s_function, + s_if, + s_import, + s_label, + s_local, + s_namespace, + s_return, + s_switch, + s_throw, + s_try, + s_while, + s_with, + s_type_script, + s_empty, + s_debugger, + s_lazy_export, + + pub fn jsonStringify(self: @This(), writer: anytype) !void { + return try writer.write(@tagName(self)); + } + + pub fn isExportLike(tag: Tag) bool { + return switch (tag) { + .s_export_clause, .s_export_default, .s_export_equals, .s_export_from, .s_export_star, .s_empty => true, + else => false, + }; + } +}; + +pub const Data = union(Tag) { + s_block: *S.Block, + s_break: *S.Break, + s_class: *S.Class, + s_comment: *S.Comment, + s_continue: *S.Continue, + s_directive: *S.Directive, + s_do_while: *S.DoWhile, + s_enum: *S.Enum, + s_export_clause: *S.ExportClause, + s_export_default: *S.ExportDefault, + s_export_equals: *S.ExportEquals, + s_export_from: *S.ExportFrom, + s_export_star: *S.ExportStar, + s_expr: *S.SExpr, + s_for_in: *S.ForIn, + s_for_of: *S.ForOf, + s_for: *S.For, + s_function: *S.Function, + s_if: *S.If, + s_import: *S.Import, + s_label: *S.Label, + s_local: *S.Local, + s_namespace: *S.Namespace, + s_return: *S.Return, + s_switch: *S.Switch, + s_throw: *S.Throw, + s_try: *S.Try, + s_while: *S.While, + s_with: *S.With, + + s_type_script: S.TypeScript, + s_empty: S.Empty, // special case, its a zero value type + s_debugger: S.Debugger, + + s_lazy_export: *Expr.Data, + + comptime { + if (@sizeOf(Stmt) > 24) { + @compileLog("Expected Stmt to be <= 24 bytes, but it is", @sizeOf(Stmt), " bytes"); + } + } + + pub const Store = struct { + const StoreType = NewStore(&.{ + S.Block, + S.Break, + S.Class, + S.Comment, + S.Continue, + S.Directive, + S.DoWhile, + S.Enum, + S.ExportClause, + S.ExportDefault, + S.ExportEquals, + S.ExportFrom, + S.ExportStar, + S.SExpr, + S.ForIn, + S.ForOf, + S.For, + S.Function, + S.If, + S.Import, + S.Label, + S.Local, + S.Namespace, + S.Return, + S.Switch, + S.Throw, + S.Try, + S.While, + S.With, + }, 128); + + pub threadlocal var instance: ?*StoreType = null; + pub threadlocal var memory_allocator: ?*ASTMemoryAllocator = null; + pub threadlocal var disable_reset = false; + + pub fn create() void { + if (instance != null or memory_allocator != null) { + return; + } + + instance = StoreType.init(); + } + + /// create || reset + pub fn begin() void { + if (memory_allocator != null) return; + if (instance == null) { + create(); + return; + } + + if (!disable_reset) + instance.?.reset(); + } + + pub fn reset() void { + if (disable_reset or memory_allocator != null) return; + instance.?.reset(); + } + + pub fn deinit() void { + if (instance == null or memory_allocator != null) return; + instance.?.deinit(); + instance = null; + } + + pub inline fn assert() void { + if (comptime Environment.allow_assert) { + if (instance == null and memory_allocator == null) + bun.unreachablePanic("Store must be init'd", .{}); + } + } + + pub fn append(comptime T: type, value: T) *T { + if (memory_allocator) |allocator| { + return allocator.append(T, value); + } + + Disabler.assert(); + return instance.?.append(T, value); + } + }; +}; + +pub fn StoredData(tag: Tag) type { + const T = @FieldType(Data, tag); + return switch (@typeInfo(T)) { + .pointer => |ptr| ptr.child, + else => T, + }; +} + +pub fn caresAboutScope(self: *Stmt) bool { + return switch (self.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 => |local| { + return local.kind != .k_var; + }, + else => { + return true; + }, + }; +} + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const Environment = bun.Environment; +const logger = bun.logger; +const string = bun.string; + +const js_ast = bun.js_ast; +const ASTMemoryAllocator = js_ast.ASTMemoryAllocator; +const Expr = js_ast.Expr; +const NewBatcher = js_ast.NewBatcher; +const NewStore = js_ast.NewStore; +const S = js_ast.S; +const Stmt = js_ast.Stmt; diff --git a/src/ast/Symbol.zig b/src/ast/Symbol.zig new file mode 100644 index 0000000000..e2a1df344d --- /dev/null +++ b/src/ast/Symbol.zig @@ -0,0 +1,489 @@ +/// This is the name that came from the parser. Printed names may be renamed +/// during minification or to avoid name collisions. Do not use the original +/// name during printing. +original_name: []const u8, + +/// This is used for symbols that represent items in the import clause of an +/// ES6 import statement. These should always be referenced by EImportIdentifier +/// instead of an EIdentifier. When this is present, the expression should +/// be printed as a property access off the namespace instead of as a bare +/// identifier. +/// +/// For correctness, this must be stored on the symbol instead of indirectly +/// associated with the Ref for the symbol somehow. In ES6 "flat bundling" +/// mode, re-exported symbols are collapsed using MergeSymbols() and renamed +/// symbols from other files that end up at this symbol must be able to tell +/// if it has a namespace alias. +namespace_alias: ?G.NamespaceAlias = null, + +/// Used by the parser for single pass parsing. +link: Ref = Ref.None, + +/// An estimate of the number of uses of this symbol. This is used to detect +/// whether a symbol is used or not. For example, TypeScript imports that are +/// unused must be removed because they are probably type-only imports. This +/// is an estimate and may not be completely accurate due to oversights in the +/// code. But it should always be non-zero when the symbol is used. +use_count_estimate: u32 = 0, + +/// This is for generating cross-chunk imports and exports for code splitting. +/// +/// Do not use this directly. Use `chunkIndex()` instead. +chunk_index: u32 = invalid_chunk_index, + +/// This is used for minification. Symbols that are declared in sibling scopes +/// can share a name. A good heuristic (from Google Closure Compiler) is to +/// assign names to symbols from sibling scopes in declaration order. That way +/// local variable names are reused in each global function like this, which +/// improves gzip compression: +/// +/// function x(a, b) { ... } +/// function y(a, b, c) { ... } +/// +/// The parser fills this in for symbols inside nested scopes. There are three +/// slot namespaces: regular symbols, label symbols, and private symbols. +/// +/// Do not use this directly. Use `nestedScopeSlot()` instead. +nested_scope_slot: u32 = invalid_nested_scope_slot, + +did_keep_name: bool = true, + +must_start_with_capital_letter_for_jsx: bool = false, + +/// The kind of symbol. This is used to determine how to print the symbol +/// and how to deal with conflicts, renaming, etc. +kind: Kind = Kind.other, + +/// Certain symbols must not be renamed or minified. For example, the +/// "arguments" variable is declared by the runtime for every function. +/// Renaming can also break any identifier used inside a "with" statement. +must_not_be_renamed: bool = false, + +/// We automatically generate import items for property accesses off of +/// namespace imports. This lets us remove the expensive namespace imports +/// while bundling in many cases, replacing them with a cheap import item +/// instead: +/// +/// import * as ns from 'path' +/// ns.foo() +/// +/// That can often be replaced by this, which avoids needing the namespace: +/// +/// import {foo} from 'path' +/// foo() +/// +/// However, if the import is actually missing then we don't want to report a +/// compile-time error like we do for real import items. This status lets us +/// avoid this. We also need to be able to replace such import items with +/// undefined, which this status is also used for. +import_item_status: ImportItemStatus = ImportItemStatus.none, + +/// --- Not actually used yet ----------------------------------------------- +/// Sometimes we lower private symbols even if they are supported. For example, +/// consider the following TypeScript code: +/// +/// class Foo { +/// #foo = 123 +/// bar = this.#foo +/// } +/// +/// If "useDefineForClassFields: false" is set in "tsconfig.json", then "bar" +/// must use assignment semantics instead of define semantics. We can compile +/// that to this code: +/// +/// class Foo { +/// constructor() { +/// this.#foo = 123; +/// this.bar = this.#foo; +/// } +/// #foo; +/// } +/// +/// However, we can't do the same for static fields: +/// +/// class Foo { +/// static #foo = 123 +/// static bar = this.#foo +/// } +/// +/// Compiling these static fields to something like this would be invalid: +/// +/// class Foo { +/// static #foo; +/// } +/// Foo.#foo = 123; +/// Foo.bar = Foo.#foo; +/// +/// Thus "#foo" must be lowered even though it's supported. Another case is +/// when we're converting top-level class declarations to class expressions +/// to avoid the TDZ and the class shadowing symbol is referenced within the +/// class body: +/// +/// class Foo { +/// static #foo = Foo +/// } +/// +/// This cannot be converted into something like this: +/// +/// var Foo = class { +/// static #foo; +/// }; +/// Foo.#foo = Foo; +/// +/// --- Not actually used yet ----------------------------------------------- +private_symbol_must_be_lowered: bool = false, + +remove_overwritten_function_declaration: bool = false, + +/// Used in HMR to decide when live binding code is needed. +has_been_assigned_to: bool = false, + +comptime { + bun.assert_eql(@sizeOf(Symbol), 88); + bun.assert_eql(@alignOf(Symbol), @alignOf([]const u8)); +} + +const invalid_chunk_index = std.math.maxInt(u32); +pub const invalid_nested_scope_slot = std.math.maxInt(u32); + +pub const SlotNamespace = enum { + must_not_be_renamed, + default, + label, + private_name, + mangled_prop, + + pub const CountsArray = std.EnumArray(SlotNamespace, u32); +}; + +/// This is for generating cross-chunk imports and exports for code splitting. +pub inline fn chunkIndex(this: *const Symbol) ?u32 { + const i = this.chunk_index; + return if (i == invalid_chunk_index) null else i; +} + +pub inline fn nestedScopeSlot(this: *const Symbol) ?u32 { + const i = this.nested_scope_slot; + return if (i == invalid_nested_scope_slot) null else i; +} + +pub fn slotNamespace(this: *const Symbol) SlotNamespace { + const kind = this.kind; + + if (kind == .unbound or this.must_not_be_renamed) { + return .must_not_be_renamed; + } + + if (kind.isPrivate()) { + return .private_name; + } + + return switch (kind) { + // .mangled_prop => .mangled_prop, + .label => .label, + else => .default, + }; +} + +pub inline fn hasLink(this: *const Symbol) bool { + return this.link.tag != .invalid; +} + +pub const Kind = enum { + /// An unbound symbol is one that isn't declared in the file it's referenced + /// in. For example, using "window" without declaring it will be unbound. + unbound, + + /// This has special merging behavior. You're allowed to re-declare these + /// symbols more than once in the same scope. These symbols are also hoisted + /// out of the scope they are declared in to the closest containing function + /// or module scope. These are the symbols with this kind: + /// + /// - Function arguments + /// - Function statements + /// - Variables declared using "var" + hoisted, + hoisted_function, + + /// There's a weird special case where catch variables declared using a simple + /// identifier (i.e. not a binding pattern) block hoisted variables instead of + /// becoming an error: + /// + /// var e = 0; + /// try { throw 1 } catch (e) { + /// print(e) // 1 + /// var e = 2 + /// print(e) // 2 + /// } + /// print(e) // 0 (since the hoisting stops at the catch block boundary) + /// + /// However, other forms are still a syntax error: + /// + /// try {} catch (e) { let e } + /// try {} catch ({e}) { var e } + /// + /// This symbol is for handling this weird special case. + catch_identifier, + + /// Generator and async functions are not hoisted, but still have special + /// properties such as being able to overwrite previous functions with the + /// same name + generator_or_async_function, + + /// This is the special "arguments" variable inside functions + arguments, + + /// Classes can merge with TypeScript namespaces. + class, + + /// A class-private identifier (i.e. "#foo"). + private_field, + private_method, + private_get, + private_set, + private_get_set_pair, + private_static_field, + private_static_method, + private_static_get, + private_static_set, + private_static_get_set_pair, + + /// Labels are in their own namespace + label, + + /// TypeScript enums can merge with TypeScript namespaces and other TypeScript + /// enums. + ts_enum, + + /// TypeScript namespaces can merge with classes, functions, TypeScript enums, + /// and other TypeScript namespaces. + ts_namespace, + + /// In TypeScript, imports are allowed to silently collide with symbols within + /// the module. Presumably this is because the imports may be type-only. + /// Import statement namespace references should NOT have this set. + import, + + /// Assigning to a "const" symbol will throw a TypeError at runtime + constant, + + // CSS identifiers that are renamed to be unique to the file they are in + local_css, + + /// This annotates all other symbols that don't have special behavior. + other, + + pub fn jsonStringify(self: @This(), writer: anytype) !void { + return try writer.write(@tagName(self)); + } + + pub inline fn isPrivate(kind: Symbol.Kind) bool { + return @intFromEnum(kind) >= @intFromEnum(Symbol.Kind.private_field) and @intFromEnum(kind) <= @intFromEnum(Symbol.Kind.private_static_get_set_pair); + } + + pub inline fn isHoisted(kind: Symbol.Kind) bool { + return switch (kind) { + .hoisted, .hoisted_function => true, + else => false, + }; + } + + pub inline fn isHoistedOrFunction(kind: Symbol.Kind) bool { + return switch (kind) { + .hoisted, .hoisted_function, .generator_or_async_function => true, + else => false, + }; + } + + pub inline fn isFunction(kind: Symbol.Kind) bool { + return switch (kind) { + .hoisted_function, .generator_or_async_function => true, + else => false, + }; + } +}; + +pub const Use = struct { + count_estimate: u32 = 0, +}; + +pub const List = BabyList(Symbol); +pub const NestedList = BabyList(List); + +pub fn mergeContentsWith(this: *Symbol, old: *Symbol) void { + this.use_count_estimate += old.use_count_estimate; + if (old.must_not_be_renamed) { + this.original_name = old.original_name; + this.must_not_be_renamed = true; + } + + // TODO: MustStartWithCapitalLetterForJSX +} + +pub const Map = struct { + // This could be represented as a "map[Ref]Symbol" but a two-level array was + // more efficient in profiles. This appears to be because it doesn't involve + // a hash. This representation also makes it trivial to quickly merge symbol + // maps from multiple files together. Each file only generates symbols in a + // single inner array, so you can join the maps together by just make a + // single outer array containing all of the inner arrays. See the comment on + // "Ref" for more detail. + symbols_for_source: NestedList = .{}, + + pub fn dump(this: Map) void { + defer Output.flush(); + for (this.symbols_for_source.slice(), 0..) |symbols, i| { + Output.prettyln("\n\n-- Source ID: {d} ({d} symbols) --\n\n", .{ i, symbols.len }); + for (symbols.slice(), 0..) |symbol, inner_index| { + Output.prettyln( + " name: {s}\n tag: {s}\n {any}\n", + .{ + symbol.original_name, @tagName(symbol.kind), + if (symbol.hasLink()) symbol.link else Ref{ + .source_index = @truncate(i), + .inner_index = @truncate(inner_index), + .tag = .symbol, + }, + }, + ); + } + } + } + + pub fn assignChunkIndex(this: *Map, decls_: DeclaredSymbol.List, chunk_index: u32) void { + const Iterator = struct { + map: *Map, + chunk_index: u32, + + pub fn next(self: @This(), ref: Ref) void { + var symbol = self.map.get(ref).?; + symbol.chunk_index = self.chunk_index; + } + }; + var decls = decls_; + + DeclaredSymbol.forEachTopLevelSymbol(&decls, Iterator{ .map = this, .chunk_index = chunk_index }, Iterator.next); + } + + pub fn merge(this: *Map, old: Ref, new: Ref) Ref { + if (old.eql(new)) { + return new; + } + + var old_symbol = this.get(old).?; + if (old_symbol.hasLink()) { + const old_link = old_symbol.link; + old_symbol.link = this.merge(old_link, new); + return old_symbol.link; + } + + var new_symbol = this.get(new).?; + + if (new_symbol.hasLink()) { + const new_link = new_symbol.link; + new_symbol.link = this.merge(old, new_link); + return new_symbol.link; + } + + old_symbol.link = new; + new_symbol.mergeContentsWith(old_symbol); + return new; + } + + pub fn get(self: *const Map, ref: Ref) ?*Symbol { + if (Ref.isSourceIndexNull(ref.sourceIndex()) or ref.isSourceContentsSlice()) { + return null; + } + + return self.symbols_for_source.at(ref.sourceIndex()).mut(ref.innerIndex()); + } + + pub fn getConst(self: *const Map, ref: Ref) ?*const Symbol { + if (Ref.isSourceIndexNull(ref.sourceIndex()) or ref.isSourceContentsSlice()) { + return null; + } + + return self.symbols_for_source.at(ref.sourceIndex()).at(ref.innerIndex()); + } + + pub fn init(sourceCount: usize, allocator: std.mem.Allocator) !Map { + const symbols_for_source: NestedList = NestedList.init(try allocator.alloc([]Symbol, sourceCount)); + return Map{ .symbols_for_source = symbols_for_source }; + } + + pub fn initWithOneList(list: List) Map { + const baby_list = BabyList(List).init((&list)[0..1]); + return initList(baby_list); + } + + pub fn initList(list: NestedList) Map { + return Map{ .symbols_for_source = list }; + } + + pub fn getWithLink(symbols: *const Map, ref: Ref) ?*Symbol { + var symbol: *Symbol = symbols.get(ref) orelse return null; + if (symbol.hasLink()) { + return symbols.get(symbol.link) orelse symbol; + } + return symbol; + } + + pub fn getWithLinkConst(symbols: *Map, ref: Ref) ?*const Symbol { + var symbol: *const Symbol = symbols.getConst(ref) orelse return null; + if (symbol.hasLink()) { + return symbols.getConst(symbol.link) orelse symbol; + } + return symbol; + } + + pub fn followAll(symbols: *Map) void { + const trace = bun.perf.trace("Symbols.followAll"); + defer trace.end(); + for (symbols.symbols_for_source.slice()) |list| { + for (list.slice()) |*symbol| { + if (!symbol.hasLink()) continue; + symbol.link = follow(symbols, symbol.link); + } + } + } + + /// Equivalent to followSymbols in esbuild + pub fn follow(symbols: *const Map, ref: Ref) Ref { + var symbol = symbols.get(ref) orelse return ref; + if (!symbol.hasLink()) { + return ref; + } + + const link = follow(symbols, symbol.link); + + if (!symbol.link.eql(link)) { + symbol.link = link; + } + + return link; + } +}; + +pub inline fn isHoisted(self: *const Symbol) bool { + return Symbol.isKindHoisted(self.kind); +} + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const BabyList = bun.BabyList; +const Output = bun.Output; + +const js_ast = bun.js_ast; +const DeclaredSymbol = js_ast.DeclaredSymbol; +const G = js_ast.G; +const ImportItemStatus = js_ast.ImportItemStatus; +const Ref = js_ast.Ref; +const Symbol = js_ast.Symbol; + +pub const isKindFunction = Symbol.Kind.isFunction; +pub const isKindHoisted = Symbol.Kind.isHoisted; +pub const isKindHoistedOrFunction = Symbol.Kind.isHoistedOrFunction; +pub const isKindPrivate = Symbol.Kind.isPrivate; diff --git a/src/ast/TS.zig b/src/ast/TS.zig new file mode 100644 index 0000000000..2e91b2fbc1 --- /dev/null +++ b/src/ast/TS.zig @@ -0,0 +1,141 @@ +/// This is for TypeScript "enum" and "namespace" blocks. Each block can +/// potentially be instantiated multiple times. The exported members of each +/// block are merged into a single namespace while the non-exported code is +/// still scoped to just within that block: +/// +/// let x = 1; +/// namespace Foo { +/// let x = 2; +/// export let y = 3; +/// } +/// namespace Foo { +/// console.log(x); // 1 +/// console.log(y); // 3 +/// } +/// +/// Doing this also works inside an enum: +/// +/// enum Foo { +/// A = 3, +/// B = A + 1, +/// } +/// enum Foo { +/// C = A + 2, +/// } +/// console.log(Foo.B) // 4 +/// console.log(Foo.C) // 5 +/// +/// This is a form of identifier lookup that works differently than the +/// hierarchical scope-based identifier lookup in JavaScript. Lookup now needs +/// to search sibling scopes in addition to parent scopes. This is accomplished +/// by sharing the map of exported members between all matching sibling scopes. +pub const TSNamespaceScope = struct { + /// This is specific to this namespace block. It's the argument of the + /// immediately-invoked function expression that the namespace block is + /// compiled into: + /// + /// var ns; + /// (function (ns2) { + /// ns2.x = 123; + /// })(ns || (ns = {})); + /// + /// This variable is "ns2" in the above example. It's the symbol to use when + /// generating property accesses off of this namespace when it's in scope. + arg_ref: Ref, + + /// This is shared between all sibling namespace blocks + exported_members: *TSNamespaceMemberMap, + + /// This is a lazily-generated map of identifiers that actually represent + /// property accesses to this namespace's properties. For example: + /// + /// namespace x { + /// export let y = 123 + /// } + /// namespace x { + /// export let z = y + /// } + /// + /// This should be compiled into the following code: + /// + /// var x; + /// (function(x2) { + /// x2.y = 123; + /// })(x || (x = {})); + /// (function(x3) { + /// x3.z = x3.y; + /// })(x || (x = {})); + /// + /// When we try to find the symbol "y", we instead return one of these lazily + /// generated proxy symbols that represent the property access "x3.y". This + /// map is unique per namespace block because "x3" is the argument symbol that + /// is specific to that particular namespace block. + property_accesses: bun.StringArrayHashMapUnmanaged(Ref) = .{}, + + /// Even though enums are like namespaces and both enums and namespaces allow + /// implicit references to properties of sibling scopes, they behave like + /// separate, er, namespaces. Implicit references only work namespace-to- + /// namespace and enum-to-enum. They do not work enum-to-namespace. And I'm + /// not sure what's supposed to happen for the namespace-to-enum case because + /// the compiler crashes: https://github.com/microsoft/TypeScript/issues/46891. + /// So basically these both work: + /// + /// enum a { b = 1 } + /// enum a { c = b } + /// + /// namespace x { export let y = 1 } + /// namespace x { export let z = y } + /// + /// This doesn't work: + /// + /// enum a { b = 1 } + /// namespace a { export let c = b } + /// + /// And this crashes the TypeScript compiler: + /// + /// namespace a { export let b = 1 } + /// enum a { c = b } + /// + /// Therefore we only allow enum/enum and namespace/namespace interactions. + is_enum_scope: bool, +}; + +pub const TSNamespaceMemberMap = bun.StringArrayHashMapUnmanaged(TSNamespaceMember); + +pub const TSNamespaceMember = struct { + loc: logger.Loc, + data: Data, + + pub const Data = union(enum) { + /// "namespace ns { export let it }" + property, + /// "namespace ns { export namespace it {} }" + namespace: *TSNamespaceMemberMap, + /// "enum ns { it }" + enum_number: f64, + /// "enum ns { it = 'it' }" + enum_string: *E.String, + /// "enum ns { it = something() }" + enum_property: void, + + pub fn isEnum(data: Data) bool { + return switch (data) { + inline else => |_, tag| comptime std.mem.startsWith(u8, @tagName(tag), "enum_"), + }; + } + }; +}; + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const logger = bun.logger; + +const js_ast = bun.js_ast; +const E = js_ast.E; +const Ref = js_ast.Ref; + +const G = js_ast.G; +pub const Class = G.Class; diff --git a/src/ast/UseDirective.zig b/src/ast/UseDirective.zig new file mode 100644 index 0000000000..03a7cfe527 --- /dev/null +++ b/src/ast/UseDirective.zig @@ -0,0 +1,66 @@ +pub const UseDirective = enum(u2) { + // TODO: Remove this, and provide `UseDirective.Optional` instead + none, + /// "use client" + client, + /// "use server" + server, + + pub const Boundering = enum(u2) { + client = @intFromEnum(UseDirective.client), + server = @intFromEnum(UseDirective.server), + }; + + pub const Flags = struct { + has_any_client: bool = false, + }; + + pub fn isBoundary(this: UseDirective, other: UseDirective) bool { + if (this == other or other == .none) + return false; + + return true; + } + + pub fn boundering(this: UseDirective, other: UseDirective) ?Boundering { + if (this == other or other == .none) + return null; + return @enumFromInt(@intFromEnum(other)); + } + + pub fn parse(contents: []const u8) ?UseDirective { + const truncated = std.mem.trimLeft(u8, contents, " \t\n\r;"); + + if (truncated.len < "'use client';".len) + return .none; + + const directive_string = truncated[0.."'use client';".len].*; + + const first_quote = directive_string[0]; + const last_quote = directive_string[directive_string.len - 2]; + if (first_quote != last_quote or (first_quote != '"' and first_quote != '\'' and first_quote != '`')) + return .none; + + const unquoted = directive_string[1 .. directive_string.len - 2]; + + if (strings.eqlComptime(unquoted, "use client")) { + return .client; + } + + if (strings.eqlComptime(unquoted, "use server")) { + return .server; + } + + return null; + } +}; + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const strings = bun.strings; + +const js_ast = bun.js_ast; +const Flags = js_ast.Flags; diff --git a/src/js_ast.zig b/src/js_ast.zig index b609627083..11cbcc9ba6 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -1,28 +1,3 @@ -const std = @import("std"); -const logger = bun.logger; -const Runtime = @import("runtime.zig").Runtime; -const bun = @import("bun"); -const string = bun.string; -const Output = bun.Output; -const Environment = bun.Environment; -const strings = bun.strings; -const MutableString = bun.MutableString; -const stringZ = bun.stringZ; -const default_allocator = bun.default_allocator; - -pub const Ref = @import("ast/base.zig").Ref; -pub const Index = @import("ast/base.zig").Index; -const RefHashCtx = @import("ast/base.zig").RefHashCtx; -const ImportRecord = @import("import_record.zig").ImportRecord; -const JSC = bun.JSC; -const JSONParser = bun.JSON; -const ComptimeStringMap = bun.ComptimeStringMap; -const JSPrinter = @import("./js_printer.zig"); -const js_lexer = @import("./js_lexer.zig"); -const TypeScript = @import("./js_parser.zig").TypeScript; -const MimeType = bun.http.MimeType; -const OOM = bun.OOM; -const Loader = bun.options.Loader; /// This is the index to the automatically-generated part containing code that /// calls "__export(exports, { ... getters ... })". This is used to generate /// getters on an exports object for ES6 export statements, and is both for @@ -30,166 +5,6 @@ const Loader = bun.options.Loader; /// although it may contain no statements if there is nothing to export. pub const namespace_export_part_index = 0; -/// This "Store" is a specialized memory allocation strategy very similar to an -/// arena, used for allocating expression and statement nodes during JavaScript -/// parsing and visiting. Allocations are grouped into large blocks, where each -/// block is treated as a fixed-buffer allocator. When a block runs out of -/// space, a new one is created; all blocks are joined as a linked list. -/// -/// Similarly to an arena, you can call .reset() to reset state, reusing memory -/// across operations. -pub fn NewStore(comptime types: []const type, comptime count: usize) type { - const largest_size, const largest_align = brk: { - var largest_size = 0; - var largest_align = 1; - for (types) |T| { - if (@sizeOf(T) == 0) { - @compileError("NewStore does not support 0 size type: " ++ @typeName(T)); - } - largest_size = @max(@sizeOf(T), largest_size); - largest_align = @max(@alignOf(T), largest_align); - } - break :brk .{ largest_size, largest_align }; - }; - - const backing_allocator = bun.default_allocator; - - const log = Output.scoped(.Store, true); - - return struct { - const Store = @This(); - - current: *Block, - debug_lock: std.debug.SafetyLock = .{}, - - pub const Block = struct { - pub const size = largest_size * count * 2; - pub const Size = std.math.IntFittingRange(0, size + largest_size); - - buffer: [size]u8 align(largest_align) = undefined, - bytes_used: Size = 0, - next: ?*Block = null, - - pub fn tryAlloc(block: *Block, comptime T: type) ?*T { - const start = std.mem.alignForward(usize, block.bytes_used, @alignOf(T)); - if (start + @sizeOf(T) > block.buffer.len) return null; - defer block.bytes_used = @intCast(start + @sizeOf(T)); - - // it's simpler to use @ptrCast, but as a sanity check, we also - // try to compute the slice. Zig will report an out of bounds - // panic if the null detection logic above is wrong - if (Environment.isDebug) { - _ = block.buffer[block.bytes_used..][0..@sizeOf(T)]; - } - - return @alignCast(@ptrCast(&block.buffer[start])); - } - }; - - const PreAlloc = struct { - metadata: Store, - first_block: Block, - }; - - pub fn firstBlock(store: *Store) *Block { - return &@as(*PreAlloc, @fieldParentPtr("metadata", store)).first_block; - } - - pub fn init() *Store { - log("init", .{}); - const prealloc = backing_allocator.create(PreAlloc) catch bun.outOfMemory(); - - prealloc.first_block.bytes_used = 0; - prealloc.first_block.next = null; - - prealloc.metadata = .{ - .current = &prealloc.first_block, - }; - - return &prealloc.metadata; - } - - pub fn deinit(store: *Store) void { - log("deinit", .{}); - var it = store.firstBlock().next; // do not free `store.head` - while (it) |next| { - if (Environment.isDebug or Environment.enable_asan) - @memset(next.buffer, undefined); - it = next.next; - backing_allocator.destroy(next); - } - - const prealloc: PreAlloc = @fieldParentPtr("metadata", store); - bun.assert(&prealloc.first_block == store.head); - backing_allocator.destroy(prealloc); - } - - pub fn reset(store: *Store) void { - log("reset", .{}); - - if (Environment.isDebug or Environment.enable_asan) { - var it: ?*Block = store.firstBlock(); - while (it) |next| : (it = next.next) { - next.bytes_used = undefined; - @memset(&next.buffer, undefined); - } - } - - store.current = store.firstBlock(); - store.current.bytes_used = 0; - } - - fn allocate(store: *Store, comptime T: type) *T { - comptime bun.assert(@sizeOf(T) > 0); // don't allocate! - comptime if (!supportsType(T)) { - @compileError("Store does not know about type: " ++ @typeName(T)); - }; - - if (store.current.tryAlloc(T)) |ptr| - return ptr; - - // a new block is needed - const next_block = if (store.current.next) |next| brk: { - next.bytes_used = 0; - break :brk next; - } else brk: { - const new_block = backing_allocator.create(Block) catch - bun.outOfMemory(); - new_block.next = null; - new_block.bytes_used = 0; - store.current.next = new_block; - break :brk new_block; - }; - - store.current = next_block; - - return next_block.tryAlloc(T) orelse - unreachable; // newly initialized blocks must have enough space for at least one - } - - pub inline fn append(store: *Store, comptime T: type, data: T) *T { - const ptr = store.allocate(T); - if (Environment.isDebug) { - log("append({s}) -> 0x{x}", .{ bun.meta.typeName(T), @intFromPtr(ptr) }); - } - ptr.* = data; - return ptr; - } - - pub fn lock(store: *Store) void { - store.debug_lock.lock(); - } - - pub fn unlock(store: *Store) void { - store.debug_lock.unlock(); - } - - fn supportsType(T: type) bool { - return std.mem.indexOfScalar(type, types, T) != null; - } - }; -} - // There are three types. // 1. Expr (expression) // 2. Stmt (statement) @@ -217,10 +32,6 @@ pub fn NewStore(comptime types: []const type, comptime count: usize) type { // But it could also be better memory locality due to smaller in-memory size (more likely to hit the cache) // only benchmarks will provide an answer! // But we must have pointers somewhere in here because can't have types that contain themselves -pub const BindingNodeIndex = Binding; -pub const StmtNodeIndex = Stmt; -pub const ExprNodeIndex = Expr; -pub const BabyList = bun.BabyList; /// Slice that stores capacity and length in the same space as a regular slice. pub const ExprNodeList = BabyList(Expr); @@ -304,246 +115,6 @@ pub const Flags = struct { }; }; -pub const Binding = struct { - loc: logger.Loc, - data: B, - - const Serializable = struct { - type: Tag, - object: string, - value: B, - loc: logger.Loc, - }; - - pub fn jsonStringify(self: *const @This(), writer: anytype) !void { - return try writer.write(Serializable{ .type = std.meta.activeTag(self.data), .object = "binding", .value = self.data, .loc = self.loc }); - } - - pub fn ToExpr(comptime expr_type: type, comptime func_type: anytype) type { - const ExprType = expr_type; - return struct { - context: *ExprType, - allocator: std.mem.Allocator, - pub const Context = @This(); - - pub fn wrapIdentifier(ctx: *const Context, loc: logger.Loc, ref: Ref) Expr { - return func_type(ctx.context, loc, ref); - } - - pub fn init(context: *ExprType) Context { - return Context{ .context = context, .allocator = context.allocator }; - } - }; - } - - pub fn toExpr(binding: *const Binding, wrapper: anytype) Expr { - const loc = binding.loc; - - switch (binding.data) { - .b_missing => { - return Expr{ .data = .{ .e_missing = E.Missing{} }, .loc = loc }; - }, - .b_identifier => |b| { - return wrapper.wrapIdentifier(loc, b.ref); - }, - .b_array => |b| { - var exprs = wrapper.allocator.alloc(Expr, b.items.len) catch unreachable; - var i: usize = 0; - while (i < exprs.len) : (i += 1) { - const item = b.items[i]; - exprs[i] = convert: { - const expr = toExpr(&item.binding, wrapper); - if (b.has_spread and i == exprs.len - 1) { - break :convert Expr.init(E.Spread, E.Spread{ .value = expr }, expr.loc); - } else if (item.default_value) |default| { - break :convert Expr.assign(expr, default); - } else { - break :convert expr; - } - }; - } - - return Expr.init(E.Array, E.Array{ .items = ExprNodeList.init(exprs), .is_single_line = b.is_single_line }, loc); - }, - .b_object => |b| { - const properties = wrapper - .allocator - .alloc(G.Property, b.properties.len) catch unreachable; - for (properties, b.properties) |*property, item| { - property.* = .{ - .flags = item.flags, - .key = item.key, - .kind = if (item.flags.contains(.is_spread)) - .spread - else - .normal, - .value = toExpr(&item.value, wrapper), - .initializer = item.default_value, - }; - } - return Expr.init( - E.Object, - E.Object{ - .properties = G.Property.List.init(properties), - .is_single_line = b.is_single_line, - }, - loc, - ); - }, - } - } - - pub const Tag = enum(u5) { - b_identifier, - b_array, - b_object, - b_missing, - - pub fn jsonStringify(self: @This(), writer: anytype) !void { - return try writer.write(@tagName(self)); - } - }; - - pub var icount: usize = 0; - - pub fn init(t: anytype, loc: logger.Loc) Binding { - icount += 1; - switch (@TypeOf(t)) { - *B.Identifier => { - return Binding{ .loc = loc, .data = B{ .b_identifier = t } }; - }, - *B.Array => { - return Binding{ .loc = loc, .data = B{ .b_array = t } }; - }, - *B.Object => { - return Binding{ .loc = loc, .data = B{ .b_object = t } }; - }, - B.Missing => { - return Binding{ .loc = loc, .data = B{ .b_missing = t } }; - }, - else => { - @compileError("Invalid type passed to Binding.init"); - }, - } - } - - pub fn alloc(allocator: std.mem.Allocator, t: anytype, loc: logger.Loc) Binding { - icount += 1; - switch (@TypeOf(t)) { - B.Identifier => { - const data = allocator.create(B.Identifier) catch unreachable; - data.* = t; - return Binding{ .loc = loc, .data = B{ .b_identifier = data } }; - }, - B.Array => { - const data = allocator.create(B.Array) catch unreachable; - data.* = t; - return Binding{ .loc = loc, .data = B{ .b_array = data } }; - }, - B.Object => { - const data = allocator.create(B.Object) catch unreachable; - data.* = t; - return Binding{ .loc = loc, .data = B{ .b_object = data } }; - }, - B.Missing => { - return Binding{ .loc = loc, .data = B{ .b_missing = .{} } }; - }, - else => { - @compileError("Invalid type passed to Binding.alloc"); - }, - } - } -}; - -/// B is for Binding! Bindings are on the left side of variable -/// declarations (s_local), which is how destructuring assignments -/// are represented in memory. Consider a basic example. -/// -/// let hello = world; -/// ^ ^ -/// | E.Identifier -/// B.Identifier -/// -/// Bindings can be nested -/// -/// B.Array -/// | B.Identifier -/// | | -/// let { foo: [ bar ] } = ... -/// ---------------- -/// B.Object -pub const B = union(Binding.Tag) { - // let x = ... - b_identifier: *B.Identifier, - // let [a, b] = ... - b_array: *B.Array, - // let { a, b: c } = ... - b_object: *B.Object, - // this is used to represent array holes - b_missing: B.Missing, - - pub const Identifier = struct { - ref: Ref, - }; - - pub const Property = struct { - flags: Flags.Property.Set = Flags.Property.None, - key: ExprNodeIndex, - value: Binding, - default_value: ?Expr = null, - }; - - pub const Object = struct { - properties: []B.Property, - is_single_line: bool = false, - - pub const Property = B.Property; - }; - - pub const Array = struct { - items: []ArrayBinding, - has_spread: bool = false, - is_single_line: bool = false, - - pub const Item = ArrayBinding; - }; - - pub const Missing = struct {}; - - /// This hash function is currently only used for React Fast Refresh transform. - /// This doesn't include the `is_single_line` properties, as they only affect whitespace. - pub fn writeToHasher(b: B, hasher: anytype, symbol_table: anytype) void { - switch (b) { - .b_identifier => |id| { - const original_name = id.ref.getSymbol(symbol_table).original_name; - writeAnyToHasher(hasher, .{ std.meta.activeTag(b), original_name.len }); - }, - .b_array => |array| { - writeAnyToHasher(hasher, .{ std.meta.activeTag(b), array.has_spread, array.items.len }); - for (array.items) |item| { - writeAnyToHasher(hasher, .{item.default_value != null}); - if (item.default_value) |default| { - default.data.writeToHasher(hasher, symbol_table); - } - item.binding.data.writeToHasher(hasher, symbol_table); - } - }, - .b_object => |object| { - writeAnyToHasher(hasher, .{ std.meta.activeTag(b), object.properties.len }); - for (object.properties) |property| { - writeAnyToHasher(hasher, .{ property.default_value != null, property.flags }); - if (property.default_value) |default| { - default.data.writeToHasher(hasher, symbol_table); - } - property.key.data.writeToHasher(hasher, symbol_table); - property.value.data.writeToHasher(hasher, symbol_table); - } - }, - .b_missing => {}, - } - } -}; - pub const ClauseItem = struct { alias: string, alias_loc: logger.Loc = logger.Loc.Empty, @@ -572,134 +143,6 @@ pub const SlotCounts = struct { } }; -const char_freq_count = 64; -pub const CharAndCount = struct { - char: u8 = 0, - count: i32 = 0, - index: usize = 0, - - pub const Array = [char_freq_count]CharAndCount; - - pub fn lessThan(_: void, a: CharAndCount, b: CharAndCount) bool { - if (a.count != b.count) { - return a.count > b.count; - } - - if (a.index != b.index) { - return a.index < b.index; - } - - return a.char < b.char; - } -}; - -pub const CharFreq = struct { - const Vector = @Vector(char_freq_count, i32); - const Buffer = [char_freq_count]i32; - - freqs: Buffer align(1) = undefined, - - const scan_big_chunk_size = 32; - pub fn scan(this: *CharFreq, text: string, delta: i32) void { - if (delta == 0) - return; - - if (text.len < scan_big_chunk_size) { - scanSmall(&this.freqs, text, delta); - } else { - scanBig(&this.freqs, text, delta); - } - } - - fn scanBig(out: *align(1) Buffer, text: string, delta: i32) void { - // https://zig.godbolt.org/z/P5dPojWGK - var freqs = out.*; - defer out.* = freqs; - var deltas: [256]i32 = [_]i32{0} ** 256; - var remain = text; - - bun.assert(remain.len >= scan_big_chunk_size); - - const unrolled = remain.len - (remain.len % scan_big_chunk_size); - const remain_end = remain.ptr + unrolled; - var unrolled_ptr = remain.ptr; - remain = remain[unrolled..]; - - while (unrolled_ptr != remain_end) : (unrolled_ptr += scan_big_chunk_size) { - const chunk = unrolled_ptr[0..scan_big_chunk_size].*; - inline for (0..scan_big_chunk_size) |i| { - deltas[@as(usize, chunk[i])] += delta; - } - } - - for (remain) |c| { - deltas[@as(usize, c)] += delta; - } - - freqs[0..26].* = deltas['a' .. 'a' + 26].*; - freqs[26 .. 26 * 2].* = deltas['A' .. 'A' + 26].*; - freqs[26 * 2 .. 62].* = deltas['0' .. '0' + 10].*; - freqs[62] = deltas['_']; - freqs[63] = deltas['$']; - } - - fn scanSmall(out: *align(1) Buffer, text: string, delta: i32) void { - var freqs: [char_freq_count]i32 = out.*; - defer out.* = freqs; - - for (text) |c| { - const i: usize = switch (c) { - 'a'...'z' => @as(usize, @intCast(c)) - 'a', - 'A'...'Z' => @as(usize, @intCast(c)) - ('A' - 26), - '0'...'9' => @as(usize, @intCast(c)) + (53 - '0'), - '_' => 62, - '$' => 63, - else => continue, - }; - freqs[i] += delta; - } - } - - pub fn include(this: *CharFreq, other: CharFreq) void { - // https://zig.godbolt.org/z/Mq8eK6K9s - const left: @Vector(char_freq_count, i32) = this.freqs; - const right: @Vector(char_freq_count, i32) = other.freqs; - - this.freqs = left + right; - } - - pub fn compile(this: *const CharFreq, allocator: std.mem.Allocator) NameMinifier { - const array: CharAndCount.Array = brk: { - var _array: CharAndCount.Array = undefined; - - for (&_array, NameMinifier.default_tail, this.freqs, 0..) |*dest, char, freq, i| { - dest.* = CharAndCount{ - .char = char, - .index = i, - .count = freq, - }; - } - - std.sort.pdq(CharAndCount, &_array, {}, CharAndCount.lessThan); - - break :brk _array; - }; - - var minifier = NameMinifier.init(allocator); - minifier.head.ensureTotalCapacityPrecise(NameMinifier.default_head.len) catch unreachable; - minifier.tail.ensureTotalCapacityPrecise(NameMinifier.default_tail.len) catch unreachable; - // TODO: investigate counting number of < 0 and > 0 and pre-allocating - for (array) |item| { - if (item.char < '0' or item.char > '9') { - minifier.head.append(item.char) catch unreachable; - } - minifier.tail.append(item.char) catch unreachable; - } - - return minifier; - } -}; - pub const NameMinifier = struct { head: std.ArrayList(u8), tail: std.ArrayList(u8), @@ -747,695 +190,6 @@ pub const NameMinifier = struct { } }; -pub const G = struct { - pub const Decl = struct { - binding: BindingNodeIndex, - value: ?ExprNodeIndex = null, - - pub const List = BabyList(Decl); - }; - - pub const NamespaceAlias = struct { - namespace_ref: Ref, - alias: string, - - was_originally_property_access: bool = false, - - import_record_index: u32 = std.math.maxInt(u32), - }; - - pub const ExportStarAlias = struct { - loc: logger.Loc, - - // Although this alias name starts off as being the same as the statement's - // namespace symbol, it may diverge if the namespace symbol name is minified. - // The original alias name is preserved here to avoid this scenario. - original_name: string, - }; - - pub const Class = struct { - class_keyword: logger.Range = logger.Range.None, - ts_decorators: ExprNodeList = ExprNodeList{}, - class_name: ?LocRef = null, - extends: ?ExprNodeIndex = null, - body_loc: logger.Loc = logger.Loc.Empty, - close_brace_loc: logger.Loc = logger.Loc.Empty, - properties: []Property = &([_]Property{}), - has_decorators: bool = false, - - pub fn canBeMoved(this: *const Class) bool { - if (this.extends != null) - return false; - - if (this.has_decorators) { - return false; - } - - for (this.properties) |property| { - if (property.kind == .class_static_block) - return false; - - const flags = property.flags; - if (flags.contains(.is_computed) or flags.contains(.is_spread)) { - return false; - } - - if (property.kind == .normal) { - if (flags.contains(.is_static)) { - for ([2]?Expr{ property.value, property.initializer }) |val_| { - if (val_) |val| { - switch (val.data) { - .e_arrow, .e_function => {}, - else => { - if (!val.canBeMoved()) { - return false; - } - }, - } - } - } - } - } - } - - return true; - } - }; - - // invalid shadowing if left as Comment - pub const Comment = struct { loc: logger.Loc, text: string }; - - pub const ClassStaticBlock = struct { - stmts: BabyList(Stmt) = .{}, - loc: logger.Loc, - }; - - pub const Property = struct { - /// This is used when parsing a pattern that uses default values: - /// - /// [a = 1] = []; - /// ({a = 1} = {}); - /// - /// It's also used for class fields: - /// - /// class Foo { a = 1 } - /// - initializer: ?ExprNodeIndex = null, - kind: Kind = .normal, - flags: Flags.Property.Set = Flags.Property.None, - - class_static_block: ?*ClassStaticBlock = null, - ts_decorators: ExprNodeList = .{}, - // Key is optional for spread - key: ?ExprNodeIndex = null, - - // This is omitted for class fields - value: ?ExprNodeIndex = null, - - ts_metadata: TypeScript.Metadata = .m_none, - - pub const List = BabyList(Property); - - pub fn deepClone(this: *const Property, allocator: std.mem.Allocator) !Property { - var class_static_block: ?*ClassStaticBlock = null; - if (this.class_static_block != null) { - class_static_block = bun.create(allocator, ClassStaticBlock, .{ - .loc = this.class_static_block.?.loc, - .stmts = try this.class_static_block.?.stmts.clone(allocator), - }); - } - return .{ - .initializer = if (this.initializer) |init| try init.deepClone(allocator) else null, - .kind = this.kind, - .flags = this.flags, - .class_static_block = class_static_block, - .ts_decorators = try this.ts_decorators.deepClone(allocator), - .key = if (this.key) |key| try key.deepClone(allocator) else null, - .value = if (this.value) |value| try value.deepClone(allocator) else null, - .ts_metadata = this.ts_metadata, - }; - } - - pub const Kind = enum(u3) { - normal, - get, - set, - spread, - declare, - abstract, - class_static_block, - - pub fn jsonStringify(self: @This(), writer: anytype) !void { - return try writer.write(@tagName(self)); - } - }; - }; - - pub const FnBody = struct { - loc: logger.Loc, - stmts: StmtNodeList, - - pub fn initReturnExpr(allocator: std.mem.Allocator, expr: Expr) !FnBody { - return .{ - .stmts = try allocator.dupe(Stmt, &.{Stmt.alloc(S.Return, .{ - .value = expr, - }, expr.loc)}), - .loc = expr.loc, - }; - } - }; - - pub const Fn = struct { - name: ?LocRef = null, - open_parens_loc: logger.Loc = logger.Loc.Empty, - args: []Arg = &.{}, - // This was originally nullable, but doing so I believe caused a miscompilation - // Specifically, the body was always null. - body: FnBody = .{ .loc = logger.Loc.Empty, .stmts = &.{} }, - arguments_ref: ?Ref = null, - - flags: Flags.Function.Set = Flags.Function.None, - - return_ts_metadata: TypeScript.Metadata = .m_none, - - pub fn deepClone(this: *const Fn, allocator: std.mem.Allocator) !Fn { - const args = try allocator.alloc(Arg, this.args.len); - for (0..args.len) |i| { - args[i] = try this.args[i].deepClone(allocator); - } - return .{ - .name = this.name, - .open_parens_loc = this.open_parens_loc, - .args = args, - .body = .{ - .loc = this.body.loc, - .stmts = this.body.stmts, - }, - .arguments_ref = this.arguments_ref, - .flags = this.flags, - .return_ts_metadata = this.return_ts_metadata, - }; - } - }; - pub const Arg = struct { - ts_decorators: ExprNodeList = ExprNodeList{}, - binding: BindingNodeIndex, - default: ?ExprNodeIndex = null, - - // "constructor(public x: boolean) {}" - is_typescript_ctor_field: bool = false, - - ts_metadata: TypeScript.Metadata = .m_none, - - pub fn deepClone(this: *const Arg, allocator: std.mem.Allocator) !Arg { - return .{ - .ts_decorators = try this.ts_decorators.deepClone(allocator), - .binding = this.binding, - .default = if (this.default) |d| try d.deepClone(allocator) else null, - .is_typescript_ctor_field = this.is_typescript_ctor_field, - .ts_metadata = this.ts_metadata, - }; - } - }; -}; - -pub const Symbol = struct { - /// This is the name that came from the parser. Printed names may be renamed - /// during minification or to avoid name collisions. Do not use the original - /// name during printing. - original_name: []const u8, - - /// This is used for symbols that represent items in the import clause of an - /// ES6 import statement. These should always be referenced by EImportIdentifier - /// instead of an EIdentifier. When this is present, the expression should - /// be printed as a property access off the namespace instead of as a bare - /// identifier. - /// - /// For correctness, this must be stored on the symbol instead of indirectly - /// associated with the Ref for the symbol somehow. In ES6 "flat bundling" - /// mode, re-exported symbols are collapsed using MergeSymbols() and renamed - /// symbols from other files that end up at this symbol must be able to tell - /// if it has a namespace alias. - namespace_alias: ?G.NamespaceAlias = null, - - /// Used by the parser for single pass parsing. - link: Ref = Ref.None, - - /// An estimate of the number of uses of this symbol. This is used to detect - /// whether a symbol is used or not. For example, TypeScript imports that are - /// unused must be removed because they are probably type-only imports. This - /// is an estimate and may not be completely accurate due to oversights in the - /// code. But it should always be non-zero when the symbol is used. - use_count_estimate: u32 = 0, - - /// This is for generating cross-chunk imports and exports for code splitting. - /// - /// Do not use this directly. Use `chunkIndex()` instead. - chunk_index: u32 = invalid_chunk_index, - - /// This is used for minification. Symbols that are declared in sibling scopes - /// can share a name. A good heuristic (from Google Closure Compiler) is to - /// assign names to symbols from sibling scopes in declaration order. That way - /// local variable names are reused in each global function like this, which - /// improves gzip compression: - /// - /// function x(a, b) { ... } - /// function y(a, b, c) { ... } - /// - /// The parser fills this in for symbols inside nested scopes. There are three - /// slot namespaces: regular symbols, label symbols, and private symbols. - /// - /// Do not use this directly. Use `nestedScopeSlot()` instead. - nested_scope_slot: u32 = invalid_nested_scope_slot, - - did_keep_name: bool = true, - - must_start_with_capital_letter_for_jsx: bool = false, - - /// The kind of symbol. This is used to determine how to print the symbol - /// and how to deal with conflicts, renaming, etc. - kind: Kind = Kind.other, - - /// Certain symbols must not be renamed or minified. For example, the - /// "arguments" variable is declared by the runtime for every function. - /// Renaming can also break any identifier used inside a "with" statement. - must_not_be_renamed: bool = false, - - /// We automatically generate import items for property accesses off of - /// namespace imports. This lets us remove the expensive namespace imports - /// while bundling in many cases, replacing them with a cheap import item - /// instead: - /// - /// import * as ns from 'path' - /// ns.foo() - /// - /// That can often be replaced by this, which avoids needing the namespace: - /// - /// import {foo} from 'path' - /// foo() - /// - /// However, if the import is actually missing then we don't want to report a - /// compile-time error like we do for real import items. This status lets us - /// avoid this. We also need to be able to replace such import items with - /// undefined, which this status is also used for. - import_item_status: ImportItemStatus = ImportItemStatus.none, - - /// --- Not actually used yet ----------------------------------------------- - /// Sometimes we lower private symbols even if they are supported. For example, - /// consider the following TypeScript code: - /// - /// class Foo { - /// #foo = 123 - /// bar = this.#foo - /// } - /// - /// If "useDefineForClassFields: false" is set in "tsconfig.json", then "bar" - /// must use assignment semantics instead of define semantics. We can compile - /// that to this code: - /// - /// class Foo { - /// constructor() { - /// this.#foo = 123; - /// this.bar = this.#foo; - /// } - /// #foo; - /// } - /// - /// However, we can't do the same for static fields: - /// - /// class Foo { - /// static #foo = 123 - /// static bar = this.#foo - /// } - /// - /// Compiling these static fields to something like this would be invalid: - /// - /// class Foo { - /// static #foo; - /// } - /// Foo.#foo = 123; - /// Foo.bar = Foo.#foo; - /// - /// Thus "#foo" must be lowered even though it's supported. Another case is - /// when we're converting top-level class declarations to class expressions - /// to avoid the TDZ and the class shadowing symbol is referenced within the - /// class body: - /// - /// class Foo { - /// static #foo = Foo - /// } - /// - /// This cannot be converted into something like this: - /// - /// var Foo = class { - /// static #foo; - /// }; - /// Foo.#foo = Foo; - /// - /// --- Not actually used yet ----------------------------------------------- - private_symbol_must_be_lowered: bool = false, - - remove_overwritten_function_declaration: bool = false, - - /// Used in HMR to decide when live binding code is needed. - has_been_assigned_to: bool = false, - - comptime { - bun.assert_eql(@sizeOf(Symbol), 88); - bun.assert_eql(@alignOf(Symbol), @alignOf([]const u8)); - } - - const invalid_chunk_index = std.math.maxInt(u32); - pub const invalid_nested_scope_slot = std.math.maxInt(u32); - - pub const SlotNamespace = enum { - must_not_be_renamed, - default, - label, - private_name, - mangled_prop, - - pub const CountsArray = std.EnumArray(SlotNamespace, u32); - }; - - /// This is for generating cross-chunk imports and exports for code splitting. - pub inline fn chunkIndex(this: *const Symbol) ?u32 { - const i = this.chunk_index; - return if (i == invalid_chunk_index) null else i; - } - - pub inline fn nestedScopeSlot(this: *const Symbol) ?u32 { - const i = this.nested_scope_slot; - return if (i == invalid_nested_scope_slot) null else i; - } - - pub fn slotNamespace(this: *const Symbol) SlotNamespace { - const kind = this.kind; - - if (kind == .unbound or this.must_not_be_renamed) { - return .must_not_be_renamed; - } - - if (kind.isPrivate()) { - return .private_name; - } - - return switch (kind) { - // .mangled_prop => .mangled_prop, - .label => .label, - else => .default, - }; - } - - pub inline fn hasLink(this: *const Symbol) bool { - return this.link.tag != .invalid; - } - - pub const Kind = enum { - /// An unbound symbol is one that isn't declared in the file it's referenced - /// in. For example, using "window" without declaring it will be unbound. - unbound, - - /// This has special merging behavior. You're allowed to re-declare these - /// symbols more than once in the same scope. These symbols are also hoisted - /// out of the scope they are declared in to the closest containing function - /// or module scope. These are the symbols with this kind: - /// - /// - Function arguments - /// - Function statements - /// - Variables declared using "var" - hoisted, - hoisted_function, - - /// There's a weird special case where catch variables declared using a simple - /// identifier (i.e. not a binding pattern) block hoisted variables instead of - /// becoming an error: - /// - /// var e = 0; - /// try { throw 1 } catch (e) { - /// print(e) // 1 - /// var e = 2 - /// print(e) // 2 - /// } - /// print(e) // 0 (since the hoisting stops at the catch block boundary) - /// - /// However, other forms are still a syntax error: - /// - /// try {} catch (e) { let e } - /// try {} catch ({e}) { var e } - /// - /// This symbol is for handling this weird special case. - catch_identifier, - - /// Generator and async functions are not hoisted, but still have special - /// properties such as being able to overwrite previous functions with the - /// same name - generator_or_async_function, - - /// This is the special "arguments" variable inside functions - arguments, - - /// Classes can merge with TypeScript namespaces. - class, - - /// A class-private identifier (i.e. "#foo"). - private_field, - private_method, - private_get, - private_set, - private_get_set_pair, - private_static_field, - private_static_method, - private_static_get, - private_static_set, - private_static_get_set_pair, - - /// Labels are in their own namespace - label, - - /// TypeScript enums can merge with TypeScript namespaces and other TypeScript - /// enums. - ts_enum, - - /// TypeScript namespaces can merge with classes, functions, TypeScript enums, - /// and other TypeScript namespaces. - ts_namespace, - - /// In TypeScript, imports are allowed to silently collide with symbols within - /// the module. Presumably this is because the imports may be type-only. - /// Import statement namespace references should NOT have this set. - import, - - /// Assigning to a "const" symbol will throw a TypeError at runtime - constant, - - // CSS identifiers that are renamed to be unique to the file they are in - local_css, - - /// This annotates all other symbols that don't have special behavior. - other, - - pub fn jsonStringify(self: @This(), writer: anytype) !void { - return try writer.write(@tagName(self)); - } - - pub inline fn isPrivate(kind: Symbol.Kind) bool { - return @intFromEnum(kind) >= @intFromEnum(Symbol.Kind.private_field) and @intFromEnum(kind) <= @intFromEnum(Symbol.Kind.private_static_get_set_pair); - } - - pub inline fn isHoisted(kind: Symbol.Kind) bool { - return switch (kind) { - .hoisted, .hoisted_function => true, - else => false, - }; - } - - pub inline fn isHoistedOrFunction(kind: Symbol.Kind) bool { - return switch (kind) { - .hoisted, .hoisted_function, .generator_or_async_function => true, - else => false, - }; - } - - pub inline fn isFunction(kind: Symbol.Kind) bool { - return switch (kind) { - .hoisted_function, .generator_or_async_function => true, - else => false, - }; - } - }; - - pub const isKindPrivate = Symbol.Kind.isPrivate; - pub const isKindHoisted = Symbol.Kind.isHoisted; - pub const isKindHoistedOrFunction = Symbol.Kind.isHoistedOrFunction; - pub const isKindFunction = Symbol.Kind.isFunction; - - pub const Use = struct { - count_estimate: u32 = 0, - }; - - pub const List = BabyList(Symbol); - pub const NestedList = BabyList(List); - - pub fn mergeContentsWith(this: *Symbol, old: *Symbol) void { - this.use_count_estimate += old.use_count_estimate; - if (old.must_not_be_renamed) { - this.original_name = old.original_name; - this.must_not_be_renamed = true; - } - - // TODO: MustStartWithCapitalLetterForJSX - } - - pub const Map = struct { - // This could be represented as a "map[Ref]Symbol" but a two-level array was - // more efficient in profiles. This appears to be because it doesn't involve - // a hash. This representation also makes it trivial to quickly merge symbol - // maps from multiple files together. Each file only generates symbols in a - // single inner array, so you can join the maps together by just make a - // single outer array containing all of the inner arrays. See the comment on - // "Ref" for more detail. - symbols_for_source: NestedList = .{}, - - pub fn dump(this: Map) void { - defer Output.flush(); - for (this.symbols_for_source.slice(), 0..) |symbols, i| { - Output.prettyln("\n\n-- Source ID: {d} ({d} symbols) --\n\n", .{ i, symbols.len }); - for (symbols.slice(), 0..) |symbol, inner_index| { - Output.prettyln( - " name: {s}\n tag: {s}\n {any}\n", - .{ - symbol.original_name, @tagName(symbol.kind), - if (symbol.hasLink()) symbol.link else Ref{ - .source_index = @truncate(i), - .inner_index = @truncate(inner_index), - .tag = .symbol, - }, - }, - ); - } - } - } - - pub fn assignChunkIndex(this: *Map, decls_: DeclaredSymbol.List, chunk_index: u32) void { - const Iterator = struct { - map: *Map, - chunk_index: u32, - - pub fn next(self: @This(), ref: Ref) void { - var symbol = self.map.get(ref).?; - symbol.chunk_index = self.chunk_index; - } - }; - var decls = decls_; - - DeclaredSymbol.forEachTopLevelSymbol(&decls, Iterator{ .map = this, .chunk_index = chunk_index }, Iterator.next); - } - - pub fn merge(this: *Map, old: Ref, new: Ref) Ref { - if (old.eql(new)) { - return new; - } - - var old_symbol = this.get(old).?; - if (old_symbol.hasLink()) { - const old_link = old_symbol.link; - old_symbol.link = this.merge(old_link, new); - return old_symbol.link; - } - - var new_symbol = this.get(new).?; - - if (new_symbol.hasLink()) { - const new_link = new_symbol.link; - new_symbol.link = this.merge(old, new_link); - return new_symbol.link; - } - - old_symbol.link = new; - new_symbol.mergeContentsWith(old_symbol); - return new; - } - - pub fn get(self: *const Map, ref: Ref) ?*Symbol { - if (Ref.isSourceIndexNull(ref.sourceIndex()) or ref.isSourceContentsSlice()) { - return null; - } - - return self.symbols_for_source.at(ref.sourceIndex()).mut(ref.innerIndex()); - } - - pub fn getConst(self: *const Map, ref: Ref) ?*const Symbol { - if (Ref.isSourceIndexNull(ref.sourceIndex()) or ref.isSourceContentsSlice()) { - return null; - } - - return self.symbols_for_source.at(ref.sourceIndex()).at(ref.innerIndex()); - } - - pub fn init(sourceCount: usize, allocator: std.mem.Allocator) !Map { - const symbols_for_source: NestedList = NestedList.init(try allocator.alloc([]Symbol, sourceCount)); - return Map{ .symbols_for_source = symbols_for_source }; - } - - pub fn initWithOneList(list: List) Map { - const baby_list = BabyList(List).init((&list)[0..1]); - return initList(baby_list); - } - - pub fn initList(list: NestedList) Map { - return Map{ .symbols_for_source = list }; - } - - pub fn getWithLink(symbols: *const Map, ref: Ref) ?*Symbol { - var symbol: *Symbol = symbols.get(ref) orelse return null; - if (symbol.hasLink()) { - return symbols.get(symbol.link) orelse symbol; - } - return symbol; - } - - pub fn getWithLinkConst(symbols: *Map, ref: Ref) ?*const Symbol { - var symbol: *const Symbol = symbols.getConst(ref) orelse return null; - if (symbol.hasLink()) { - return symbols.getConst(symbol.link) orelse symbol; - } - return symbol; - } - - pub fn followAll(symbols: *Map) void { - const trace = bun.perf.trace("Symbols.followAll"); - defer trace.end(); - for (symbols.symbols_for_source.slice()) |list| { - for (list.slice()) |*symbol| { - if (!symbol.hasLink()) continue; - symbol.link = follow(symbols, symbol.link); - } - } - } - - /// Equivalent to followSymbols in esbuild - pub fn follow(symbols: *const Map, ref: Ref) Ref { - var symbol = symbols.get(ref) orelse return ref; - if (!symbol.hasLink()) { - return ref; - } - - const link = follow(symbols, symbol.link); - - if (!symbol.link.eql(link)) { - symbol.link = link; - } - - return link; - } - }; - - pub inline fn isHoisted(self: *const Symbol) bool { - return Symbol.isKindHoisted(self.kind); - } -}; - pub const OptionalChain = enum(u1) { /// "a?.b" start, @@ -1449,5024 +203,6 @@ pub const OptionalChain = enum(u1) { } }; -pub const E = struct { - /// This represents an internal property name that can be mangled. The symbol - /// referenced by this expression should be a "SymbolMangledProp" symbol. - pub const NameOfSymbol = struct { - ref: Ref = Ref.None, - - /// If true, a preceding comment contains "@__KEY__" - /// - /// Currently not used - has_property_key_comment: bool = false, - }; - - pub const Array = struct { - items: ExprNodeList = ExprNodeList{}, - comma_after_spread: ?logger.Loc = null, - is_single_line: bool = false, - is_parenthesized: bool = false, - was_originally_macro: bool = false, - close_bracket_loc: logger.Loc = logger.Loc.Empty, - - pub fn push(this: *Array, allocator: std.mem.Allocator, item: Expr) !void { - try this.items.push(allocator, item); - } - - pub inline fn slice(this: Array) []Expr { - return this.items.slice(); - } - - pub fn inlineSpreadOfArrayLiterals( - this: *Array, - allocator: std.mem.Allocator, - estimated_count: usize, - ) !ExprNodeList { - var out = try allocator.alloc( - Expr, - // This over-allocates a little but it's fine - estimated_count + @as(usize, this.items.len), - ); - var remain = out; - for (this.items.slice()) |item| { - switch (item.data) { - .e_spread => |val| { - if (val.value.data == .e_array) { - for (val.value.data.e_array.items.slice()) |inner_item| { - if (inner_item.data == .e_missing) { - remain[0] = Expr.init(E.Undefined, .{}, inner_item.loc); - remain = remain[1..]; - } else { - remain[0] = inner_item; - remain = remain[1..]; - } - } - - // skip empty arrays - // don't include the inlined spread. - continue; - } - // non-arrays are kept in - }, - else => {}, - } - - remain[0] = item; - remain = remain[1..]; - } - - return ExprNodeList.init(out[0 .. out.len - remain.len]); - } - - pub fn toJS(this: @This(), allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) ToJSError!JSC.JSValue { - const items = this.items.slice(); - var array = try JSC.JSValue.createEmptyArray(globalObject, items.len); - array.protect(); - defer array.unprotect(); - for (items, 0..) |expr, j| { - array.putIndex(globalObject, @as(u32, @truncate(j)), try expr.data.toJS(allocator, globalObject)); - } - - return array; - } - - /// Assumes each item in the array is a string - pub fn alphabetizeStrings(this: *Array) void { - if (comptime Environment.allow_assert) { - for (this.items.slice()) |item| { - bun.assert(item.data == .e_string); - } - } - std.sort.pdq(Expr, this.items.slice(), {}, Sorter.isLessThan); - } - - const Sorter = struct { - pub fn isLessThan(ctx: void, lhs: Expr, rhs: Expr) bool { - return strings.cmpStringsAsc(ctx, lhs.data.e_string.data, rhs.data.e_string.data); - } - }; - }; - - pub const Unary = struct { - op: Op.Code, - value: ExprNodeIndex, - }; - - pub const Binary = struct { - left: ExprNodeIndex, - right: ExprNodeIndex, - op: Op.Code, - }; - - pub const Boolean = struct { - value: bool, - pub fn toJS(this: @This(), ctx: *JSC.JSGlobalObject) JSC.C.JSValueRef { - return JSC.C.JSValueMakeBoolean(ctx, this.value); - } - }; - pub const Super = struct {}; - pub const Null = struct {}; - pub const This = struct {}; - pub const Undefined = struct {}; - pub const New = struct { - target: ExprNodeIndex, - args: ExprNodeList = ExprNodeList{}, - - // True if there is a comment containing "@__PURE__" or "#__PURE__" preceding - // this call expression. See the comment inside ECall for more details. - can_be_unwrapped_if_unused: bool = false, - - close_parens_loc: logger.Loc, - }; - pub const NewTarget = struct { - range: logger.Range, - }; - pub const ImportMeta = struct {}; - pub const ImportMetaMain = struct { - /// If we want to print `!import.meta.main`, set this flag to true - /// instead of wrapping in a unary not. This way, the printer can easily - /// print `require.main != module` instead of `!(require.main == module)` - inverted: bool = false, - }; - - pub const Special = union(enum) { - /// emits `exports` or `module.exports` depending on `commonjs_named_exports_deoptimized` - module_exports, - /// `import.meta.hot` - hot_enabled, - /// Acts as .e_undefined, but allows property accesses to the rest of the HMR API. - hot_disabled, - /// `import.meta.hot.data` when HMR is enabled. Not reachable when it is disabled. - hot_data, - /// `import.meta.hot.accept` when HMR is enabled. Truthy. - hot_accept, - /// Converted from `hot_accept` to this in js_parser.zig when it is - /// passed strings. Printed as `hmr.hot.acceptSpecifiers` - hot_accept_visited, - /// Prints the resolved specifier string for an import record. - resolved_specifier_string: ImportRecord.Index, - }; - - pub const Call = struct { - // Node: - target: ExprNodeIndex, - args: ExprNodeList = ExprNodeList{}, - optional_chain: ?OptionalChain = null, - is_direct_eval: bool = false, - close_paren_loc: logger.Loc = logger.Loc.Empty, - - // True if there is a comment containing "@__PURE__" or "#__PURE__" preceding - // this call expression. This is an annotation used for tree shaking, and - // means that the call can be removed if it's unused. It does not mean the - // call is pure (e.g. it may still return something different if called twice). - // - // Note that the arguments are not considered to be part of the call. If the - // call itself is removed due to this annotation, the arguments must remain - // if they have side effects. - can_be_unwrapped_if_unused: bool = false, - - // Used when printing to generate the source prop on the fly - was_jsx_element: bool = false, - - pub fn hasSameFlagsAs(a: *Call, b: *Call) bool { - return (a.optional_chain == b.optional_chain and - a.is_direct_eval == b.is_direct_eval and - a.can_be_unwrapped_if_unused == b.can_be_unwrapped_if_unused); - } - }; - - pub const Dot = struct { - // target is Node - target: ExprNodeIndex, - name: string, - name_loc: logger.Loc, - optional_chain: ?OptionalChain = null, - - // If true, this property access is known to be free of side-effects. That - // means it can be removed if the resulting value isn't used. - can_be_removed_if_unused: bool = false, - - // If true, this property access is a function that, when called, can be - // unwrapped if the resulting value is unused. Unwrapping means discarding - // the call target but keeping any arguments with side effects. - call_can_be_unwrapped_if_unused: bool = false, - - pub fn hasSameFlagsAs(a: *Dot, b: *Dot) bool { - return (a.optional_chain == b.optional_chain and - a.is_direct_eval == b.is_direct_eval and - a.can_be_removed_if_unused == b.can_be_removed_if_unused and a.call_can_be_unwrapped_if_unused == b.call_can_be_unwrapped_if_unused); - } - }; - - pub const Index = struct { - index: ExprNodeIndex, - target: ExprNodeIndex, - optional_chain: ?OptionalChain = null, - - pub fn hasSameFlagsAs(a: *E.Index, b: *E.Index) bool { - return (a.optional_chain == b.optional_chain); - } - }; - - pub const Arrow = struct { - args: []G.Arg = &[_]G.Arg{}, - body: G.FnBody, - - is_async: bool = false, - has_rest_arg: bool = false, - prefer_expr: bool = false, // Use shorthand if true and "Body" is a single return statement - - pub const noop_return_undefined: Arrow = .{ - .args = &.{}, - .body = .{ - .loc = .Empty, - .stmts = &.{}, - }, - }; - }; - - pub const Function = struct { func: G.Fn }; - - pub const Identifier = struct { - ref: Ref = Ref.None, - - // If we're inside a "with" statement, this identifier may be a property - // access. In that case it would be incorrect to remove this identifier since - // the property access may be a getter or setter with side effects. - must_keep_due_to_with_stmt: bool = false, - - // If true, this identifier is known to not have a side effect (i.e. to not - // throw an exception) when referenced. If false, this identifier may or - // not have side effects when referenced. This is used to allow the removal - // of known globals such as "Object" if they aren't used. - can_be_removed_if_unused: bool = false, - - // If true, this identifier represents a function that, when called, can be - // unwrapped if the resulting value is unused. Unwrapping means discarding - // the call target but keeping any arguments with side effects. - call_can_be_unwrapped_if_unused: bool = false, - - pub inline fn init(ref: Ref) Identifier { - return Identifier{ - .ref = ref, - .must_keep_due_to_with_stmt = false, - .can_be_removed_if_unused = false, - .call_can_be_unwrapped_if_unused = false, - }; - } - }; - - /// This is similar to an `Identifier` but it represents a reference to an ES6 - /// import item. - /// - /// Depending on how the code is linked, the file containing this EImportIdentifier - /// may or may not be in the same module group as the file it was imported from. - /// - /// If it's the same module group than we can just merge the import item symbol - /// with the corresponding symbol that was imported, effectively renaming them - /// to be the same thing and statically binding them together. - /// - /// But if it's a different module group, then the import must be dynamically - /// evaluated using a property access off the corresponding namespace symbol, - /// which represents the result of a require() call. - /// - /// It's stored as a separate type so it's not easy to confuse with a plain - /// identifier. For example, it'd be bad if code trying to convert "{x: x}" into - /// "{x}" shorthand syntax wasn't aware that the "x" in this case is actually - /// "{x: importedNamespace.x}". This separate type forces code to opt-in to - /// doing this instead of opt-out. - pub const ImportIdentifier = struct { - ref: Ref = Ref.None, - - /// If true, this was originally an identifier expression such as "foo". If - /// false, this could potentially have been a member access expression such - /// as "ns.foo" off of an imported namespace object. - was_originally_identifier: bool = false, - }; - - /// This is a dot expression on exports, such as `exports.`. It is given - /// it's own AST node to allow CommonJS unwrapping, in which this can just be - /// the identifier in the Ref - pub const CommonJSExportIdentifier = struct { - ref: Ref = Ref.None, - base: Base = .exports, - - /// The original variant of the dot expression must be known so that in the case that we - /// - fail to convert this to ESM - /// - ALSO see an assignment to `module.exports` (commonjs_module_exports_assigned_deoptimized) - /// It must be known if `exports` or `module.exports` was written in source - /// code, as the distinction will alter behavior. The fixup happens in the printer when - /// printing this node. - pub const Base = enum { - exports, - module_dot_exports, - }; - }; - - // This is similar to EIdentifier but it represents class-private fields and - // methods. It can be used where computed properties can be used, such as - // EIndex and Property. - pub const PrivateIdentifier = struct { - ref: Ref, - }; - - /// In development mode, the new JSX transform has a few special props - /// - `React.jsxDEV(type, arguments, key, isStaticChildren, source, self)` - /// - `arguments`: - /// ```{ ...props, children: children, }``` - /// - `source`: https://github.com/babel/babel/blob/ef87648f3f05ccc393f89dea7d4c7c57abf398ce/packages/babel-plugin-transform-react-jsx-source/src/index.js#L24-L48 - /// ```{ - /// fileName: string | null, - /// columnNumber: number | null, - /// lineNumber: number | null, - /// }``` - /// - `children`: - /// - static the function is React.jsxsDEV, "jsxs" instead of "jsx" - /// - one child? the function is React.jsxDEV, - /// - no children? the function is React.jsxDEV and children is an empty array. - /// `isStaticChildren`: https://github.com/facebook/react/blob/4ca62cac45c288878d2532e5056981d177f9fdac/packages/react/src/jsx/ReactJSXElementValidator.js#L369-L384 - /// This flag means children is an array of JSX Elements literals. - /// The documentation on this is sparse, but it appears that - /// React just calls Object.freeze on the children array. - /// Object.freeze, historically, is quite a bit slower[0] than just not doing that. - /// Given that...I am choosing to always pass "false" to this. - /// This also skips extra state that we'd need to track. - /// If React Fast Refresh ends up using this later, then we can revisit this decision. - /// [0]: https://github.com/automerge/automerge/issues/177 - pub const JSXElement = struct { - /// JSX tag name - ///
=> E.String.init("div") - /// => E.Identifier{.ref = symbolPointingToMyComponent } - /// null represents a fragment - tag: ?ExprNodeIndex = null, - - /// JSX props - properties: G.Property.List = G.Property.List{}, - - /// JSX element children
{this_is_a_child_element}
- children: ExprNodeList = ExprNodeList{}, - - // needed to make sure parse and visit happen in the same order - key_prop_index: i32 = -1, - - flags: Flags.JSXElement.Bitset = Flags.JSXElement.Bitset{}, - - close_tag_loc: logger.Loc = logger.Loc.Empty, - - pub const SpecialProp = enum { - __self, // old react transform used this as a prop - __source, - key, - ref, - any, - - pub const Map = ComptimeStringMap(SpecialProp, .{ - .{ "__self", .__self }, - .{ "__source", .__source }, - .{ "key", .key }, - .{ "ref", .ref }, - }); - }; - }; - - pub const Missing = struct { - pub fn jsonStringify(_: *const @This(), writer: anytype) !void { - return try writer.write(null); - } - }; - - pub const Number = struct { - value: f64, - - const double_digit = [_]string{ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "100" }; - const neg_double_digit = [_]string{ "-0", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9", "-10", "-11", "-12", "-13", "-14", "-15", "-16", "-17", "-18", "-19", "-20", "-21", "-22", "-23", "-24", "-25", "-26", "-27", "-28", "-29", "-30", "-31", "-32", "-33", "-34", "-35", "-36", "-37", "-38", "-39", "-40", "-41", "-42", "-43", "-44", "-45", "-46", "-47", "-48", "-49", "-50", "-51", "-52", "-53", "-54", "-55", "-56", "-57", "-58", "-59", "-60", "-61", "-62", "-63", "-64", "-65", "-66", "-67", "-68", "-69", "-70", "-71", "-72", "-73", "-74", "-75", "-76", "-77", "-78", "-79", "-80", "-81", "-82", "-83", "-84", "-85", "-86", "-87", "-88", "-89", "-90", "-91", "-92", "-93", "-94", "-95", "-96", "-97", "-98", "-99", "-100" }; - - /// String concatenation with numbers is required by the TypeScript compiler for - /// "constant expression" handling in enums. We can match the behavior of a JS VM - /// by calling out to the APIs in WebKit which are responsible for this operation. - /// - /// This can return `null` in wasm builds to avoid linking JSC - pub fn toString(this: Number, allocator: std.mem.Allocator) ?string { - return toStringFromF64(this.value, allocator); - } - - pub fn toStringFromF64(value: f64, allocator: std.mem.Allocator) ?string { - if (value == @trunc(value) and (value < std.math.maxInt(i32) and value > std.math.minInt(i32))) { - const int_value = @as(i64, @intFromFloat(value)); - const abs = @as(u64, @intCast(@abs(int_value))); - - // do not allocate for a small set of constant numbers: -100 through 100 - if (abs < double_digit.len) { - return if (int_value < 0) - neg_double_digit[abs] - else - double_digit[abs]; - } - - return std.fmt.allocPrint(allocator, "{d}", .{@as(i32, @intCast(int_value))}) catch return null; - } - - if (std.math.isNan(value)) { - return "NaN"; - } - - if (std.math.isNegativeInf(value)) { - return "-Infinity"; - } - - if (std.math.isInf(value)) { - return "Infinity"; - } - - if (Environment.isNative) { - var buf: [124]u8 = undefined; - return allocator.dupe(u8, bun.fmt.FormatDouble.dtoa(&buf, value)) catch bun.outOfMemory(); - } else { - // do not attempt to implement the spec here, it would be error prone. - } - - return null; - } - - pub inline fn toU64(self: Number) u64 { - return self.to(u64); - } - - pub inline fn toUsize(self: Number) usize { - return self.to(usize); - } - - pub inline fn toU32(self: Number) u32 { - return self.to(u32); - } - - pub inline fn toU16(self: Number) u16 { - return self.to(u16); - } - - pub fn to(self: Number, comptime T: type) T { - return @as(T, @intFromFloat(@min(@max(@trunc(self.value), 0), comptime @min(std.math.floatMax(f64), std.math.maxInt(T))))); - } - - pub fn jsonStringify(self: *const Number, writer: anytype) !void { - return try writer.write(self.value); - } - - pub fn toJS(this: @This()) JSC.JSValue { - return JSC.JSValue.jsNumber(this.value); - } - }; - - pub const BigInt = struct { - value: string, - - pub var empty = BigInt{ .value = "" }; - - pub fn jsonStringify(self: *const @This(), writer: anytype) !void { - return try writer.write(self.value); - } - - pub fn toJS(_: @This()) JSC.JSValue { - // TODO: - return JSC.JSValue.jsNumber(0); - } - }; - - pub const Object = struct { - properties: G.Property.List = G.Property.List{}, - comma_after_spread: ?logger.Loc = null, - is_single_line: bool = false, - is_parenthesized: bool = false, - was_originally_macro: bool = false, - - close_brace_loc: logger.Loc = logger.Loc.Empty, - - // used in TOML parser to merge properties - pub const Rope = struct { - head: Expr, - next: ?*Rope = null, - pub fn append(this: *Rope, expr: Expr, allocator: std.mem.Allocator) OOM!*Rope { - if (this.next) |next| { - return try next.append(expr, allocator); - } - - const rope = try allocator.create(Rope); - rope.* = .{ .head = expr }; - this.next = rope; - return rope; - } - }; - - pub fn get(self: *const Object, key: string) ?Expr { - return if (asProperty(self, key)) |query| query.expr else @as(?Expr, null); - } - - pub fn toJS(this: *Object, allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) ToJSError!JSC.JSValue { - var obj = JSC.JSValue.createEmptyObject(globalObject, this.properties.len); - obj.protect(); - defer obj.unprotect(); - const props: []const G.Property = this.properties.slice(); - for (props) |prop| { - if (prop.kind != .normal or prop.class_static_block != null or prop.key == null or prop.value == null) { - return error.@"Cannot convert argument type to JS"; - } - const key = try prop.key.?.data.toJS(allocator, globalObject); - const value = try prop.value.?.toJS(allocator, globalObject); - try obj.putToPropertyKey(globalObject, key, value); - } - - return obj; - } - - pub fn put(self: *Object, allocator: std.mem.Allocator, key: string, expr: Expr) !void { - if (asProperty(self, key)) |query| { - self.properties.ptr[query.i].value = expr; - } else { - try self.properties.push(allocator, .{ - .key = Expr.init(E.String, E.String.init(key), expr.loc), - .value = expr, - }); - } - } - - pub fn putString(self: *Object, allocator: std.mem.Allocator, key: string, value: string) !void { - return try put(self, allocator, key, Expr.init(E.String, E.String.init(value), logger.Loc.Empty)); - } - - pub const SetError = error{ OutOfMemory, Clobber }; - - pub fn set(self: *const Object, key: Expr, allocator: std.mem.Allocator, value: Expr) SetError!void { - if (self.hasProperty(key.data.e_string.data)) return error.Clobber; - try self.properties.push(allocator, .{ - .key = key, - .value = value, - }); - } - - pub const RopeQuery = struct { - expr: Expr, - rope: *const Rope, - }; - - // this is terribly, shamefully slow - pub fn setRope(self: *Object, rope: *const Rope, allocator: std.mem.Allocator, value: Expr) SetError!void { - if (self.get(rope.head.data.e_string.data)) |existing| { - switch (existing.data) { - .e_array => |array| { - if (rope.next == null) { - try array.push(allocator, value); - return; - } - - if (array.items.last()) |last| { - if (last.data != .e_object) { - return error.Clobber; - } - - try last.data.e_object.setRope(rope.next.?, allocator, value); - return; - } - - try array.push(allocator, value); - return; - }, - .e_object => |object| { - if (rope.next != null) { - try object.setRope(rope.next.?, allocator, value); - return; - } - - return error.Clobber; - }, - else => { - return error.Clobber; - }, - } - } - - var value_ = value; - if (rope.next) |next| { - var obj = Expr.init(E.Object, E.Object{ .properties = .{} }, rope.head.loc); - try obj.data.e_object.setRope(next, allocator, value); - value_ = obj; - } - - try self.properties.push(allocator, .{ - .key = rope.head, - .value = value_, - }); - } - - pub fn getOrPutObject(self: *Object, rope: *const Rope, allocator: std.mem.Allocator) SetError!Expr { - if (self.get(rope.head.data.e_string.data)) |existing| { - switch (existing.data) { - .e_array => |array| { - if (rope.next == null) { - return error.Clobber; - } - - if (array.items.last()) |last| { - if (last.data != .e_object) { - return error.Clobber; - } - - return try last.data.e_object.getOrPutObject(rope.next.?, allocator); - } - - return error.Clobber; - }, - .e_object => |object| { - if (rope.next != null) { - return try object.getOrPutObject(rope.next.?, allocator); - } - - // success - return existing; - }, - else => { - return error.Clobber; - }, - } - } - - if (rope.next) |next| { - var obj = Expr.init(E.Object, E.Object{ .properties = .{} }, rope.head.loc); - const out = try obj.data.e_object.getOrPutObject(next, allocator); - try self.properties.push(allocator, .{ - .key = rope.head, - .value = obj, - }); - return out; - } - - const out = Expr.init(E.Object, E.Object{}, rope.head.loc); - try self.properties.push(allocator, .{ - .key = rope.head, - .value = out, - }); - return out; - } - - pub fn getOrPutArray(self: *Object, rope: *const Rope, allocator: std.mem.Allocator) SetError!Expr { - if (self.get(rope.head.data.e_string.data)) |existing| { - switch (existing.data) { - .e_array => |array| { - if (rope.next == null) { - return existing; - } - - if (array.items.last()) |last| { - if (last.data != .e_object) { - return error.Clobber; - } - - return try last.data.e_object.getOrPutArray(rope.next.?, allocator); - } - - return error.Clobber; - }, - .e_object => |object| { - if (rope.next == null) { - return error.Clobber; - } - - return try object.getOrPutArray(rope.next.?, allocator); - }, - else => { - return error.Clobber; - }, - } - } - - if (rope.next) |next| { - var obj = Expr.init(E.Object, E.Object{ .properties = .{} }, rope.head.loc); - const out = try obj.data.e_object.getOrPutArray(next, allocator); - try self.properties.push(allocator, .{ - .key = rope.head, - .value = obj, - }); - return out; - } - - const out = Expr.init(E.Array, E.Array{}, rope.head.loc); - try self.properties.push(allocator, .{ - .key = rope.head, - .value = out, - }); - return out; - } - - pub fn hasProperty(obj: *const Object, name: string) bool { - for (obj.properties.slice()) |prop| { - const key = prop.key orelse continue; - if (std.meta.activeTag(key.data) != .e_string) continue; - if (key.data.e_string.eql(string, name)) return true; - } - return false; - } - - pub fn asProperty(obj: *const Object, name: string) ?Expr.Query { - for (obj.properties.slice(), 0..) |prop, i| { - const value = prop.value orelse continue; - const key = prop.key orelse continue; - if (std.meta.activeTag(key.data) != .e_string) continue; - const key_str = key.data.e_string; - if (key_str.eql(string, name)) { - return Expr.Query{ - .expr = value, - .loc = key.loc, - .i = @as(u32, @truncate(i)), - }; - } - } - - return null; - } - - /// Assumes each key in the property is a string - pub fn alphabetizeProperties(this: *Object) void { - if (comptime Environment.isDebug) { - for (this.properties.slice()) |prop| { - bun.assert(prop.key.?.data == .e_string); - } - } - std.sort.pdq(G.Property, this.properties.slice(), {}, Sorter.isLessThan); - } - - pub fn packageJSONSort(this: *Object) void { - std.sort.pdq(G.Property, this.properties.slice(), {}, PackageJSONSort.Fields.isLessThan); - } - - const PackageJSONSort = struct { - const Fields = enum(u8) { - name = 0, - version = 1, - author = 2, - repository = 3, - config = 4, - main = 5, - module = 6, - dependencies = 7, - devDependencies = 8, - optionalDependencies = 9, - peerDependencies = 10, - exports = 11, - __fake = 12, - - pub const Map = ComptimeStringMap(Fields, .{ - .{ "name", Fields.name }, - .{ "version", Fields.version }, - .{ "author", Fields.author }, - .{ "repository", Fields.repository }, - .{ "config", Fields.config }, - .{ "main", Fields.main }, - .{ "module", Fields.module }, - .{ "dependencies", Fields.dependencies }, - .{ "devDependencies", Fields.devDependencies }, - .{ "optionalDependencies", Fields.optionalDependencies }, - .{ "peerDependencies", Fields.peerDependencies }, - .{ "exports", Fields.exports }, - }); - - pub fn isLessThan(ctx: void, lhs: G.Property, rhs: G.Property) bool { - var lhs_key_size: u8 = @intFromEnum(Fields.__fake); - var rhs_key_size: u8 = @intFromEnum(Fields.__fake); - - if (lhs.key != null and lhs.key.?.data == .e_string) { - lhs_key_size = @intFromEnum(Map.get(lhs.key.?.data.e_string.data) orelse Fields.__fake); - } - - if (rhs.key != null and rhs.key.?.data == .e_string) { - rhs_key_size = @intFromEnum(Map.get(rhs.key.?.data.e_string.data) orelse Fields.__fake); - } - - return switch (std.math.order(lhs_key_size, rhs_key_size)) { - .lt => true, - .gt => false, - .eq => strings.cmpStringsAsc(ctx, lhs.key.?.data.e_string.data, rhs.key.?.data.e_string.data), - }; - } - }; - }; - - const Sorter = struct { - pub fn isLessThan(ctx: void, lhs: G.Property, rhs: G.Property) bool { - return strings.cmpStringsAsc(ctx, lhs.key.?.data.e_string.data, rhs.key.?.data.e_string.data); - } - }; - }; - - pub const Spread = struct { value: ExprNodeIndex }; - - /// JavaScript string literal type - pub const String = struct { - // A version of this where `utf8` and `value` are stored in a packed union, with len as a single u32 was attempted. - // It did not improve benchmarks. Neither did converting this from a heap-allocated type to a stack-allocated type. - // TODO: change this to *const anyopaque and change all uses to either .slice8() or .slice16() - data: []const u8 = "", - prefer_template: bool = false, - - // A very simple rope implementation - // We only use this for string folding, so this is kind of overkill - // We don't need to deal with substrings - next: ?*String = null, - end: ?*String = null, - rope_len: u32 = 0, - is_utf16: bool = false, - - pub fn isIdentifier(this: *String, allocator: std.mem.Allocator) bool { - if (!this.isUTF8()) { - return bun.js_lexer.isIdentifierUTF16(this.slice16()); - } - - return bun.js_lexer.isIdentifier(this.slice(allocator)); - } - - pub const class = E.String{ .data = "class" }; - - pub fn push(this: *String, other: *String) void { - bun.assert(this.isUTF8()); - bun.assert(other.isUTF8()); - - if (other.rope_len == 0) { - other.rope_len = @truncate(other.data.len); - } - - if (this.rope_len == 0) { - this.rope_len = @truncate(this.data.len); - } - - this.rope_len += other.rope_len; - if (this.next == null) { - this.next = other; - this.end = other; - } else { - var end = this.end.?; - while (end.next != null) end = end.end.?; - end.next = other; - this.end = other; - } - } - - /// Cloning the rope string is rarely needed, see `foldStringAddition`'s - /// comments and the 'edgecase/EnumInliningRopeStringPoison' test - pub fn cloneRopeNodes(s: String) String { - var root = s; - - if (root.next != null) { - var current: ?*String = &root; - while (true) { - const node = current.?; - if (node.next) |next| { - node.next = Expr.Data.Store.append(String, next.*); - current = node.next; - } else { - root.end = node; - break; - } - } - } - - return root; - } - - pub fn toUTF8(this: *String, allocator: std.mem.Allocator) !void { - if (!this.is_utf16) return; - this.data = try strings.toUTF8Alloc(allocator, this.slice16()); - this.is_utf16 = false; - } - - pub fn init(value: anytype) String { - const Value = @TypeOf(value); - if (Value == []u16 or Value == []const u16) { - return .{ - .data = @as([*]const u8, @ptrCast(value.ptr))[0..value.len], - .is_utf16 = true, - }; - } - - return .{ .data = value }; - } - - /// E.String containing non-ascii characters may not fully work. - /// https://github.com/oven-sh/bun/issues/11963 - /// More investigation is needed. - pub fn initReEncodeUTF8(utf8: []const u8, allocator: std.mem.Allocator) String { - return if (bun.strings.isAllASCII(utf8)) - init(utf8) - else - init(bun.strings.toUTF16AllocForReal(allocator, utf8, false, false) catch bun.outOfMemory()); - } - - pub fn slice8(this: *const String) []const u8 { - bun.assert(!this.is_utf16); - return this.data; - } - - pub fn slice16(this: *const String) []const u16 { - bun.assert(this.is_utf16); - return @as([*]const u16, @ptrCast(@alignCast(this.data.ptr)))[0..this.data.len]; - } - - pub fn resolveRopeIfNeeded(this: *String, allocator: std.mem.Allocator) void { - if (this.next == null or !this.isUTF8()) return; - var bytes = std.ArrayList(u8).initCapacity(allocator, this.rope_len) catch bun.outOfMemory(); - - bytes.appendSliceAssumeCapacity(this.data); - var str = this.next; - while (str) |part| { - bytes.appendSlice(part.data) catch bun.outOfMemory(); - str = part.next; - } - this.data = bytes.items; - this.next = null; - } - - pub fn slice(this: *String, allocator: std.mem.Allocator) []const u8 { - this.resolveRopeIfNeeded(allocator); - return this.string(allocator) catch bun.outOfMemory(); - } - - pub var empty = String{}; - pub var @"true" = String{ .data = "true" }; - pub var @"false" = String{ .data = "false" }; - pub var @"null" = String{ .data = "null" }; - pub var @"undefined" = String{ .data = "undefined" }; - - pub fn clone(str: *const String, allocator: std.mem.Allocator) !String { - return String{ - .data = try allocator.dupe(u8, str.data), - .prefer_template = str.prefer_template, - .is_utf16 = !str.isUTF8(), - }; - } - - pub fn cloneSliceIfNecessary(str: *const String, allocator: std.mem.Allocator) !bun.string { - if (str.isUTF8()) { - return allocator.dupe(u8, str.string(allocator) catch unreachable); - } - - return str.string(allocator); - } - - pub fn javascriptLength(s: *const String) ?u32 { - if (s.rope_len > 0) { - // We only support ascii ropes for now - return s.rope_len; - } - - if (s.isUTF8()) { - if (!strings.isAllASCII(s.data)) { - return null; - } - return @truncate(s.data.len); - } - - return @truncate(s.slice16().len); - } - - pub inline fn len(s: *const String) usize { - return if (s.rope_len > 0) s.rope_len else s.data.len; - } - - pub inline fn isUTF8(s: *const String) bool { - return !s.is_utf16; - } - - pub inline fn isBlank(s: *const String) bool { - return s.len() == 0; - } - - pub inline fn isPresent(s: *const String) bool { - return s.len() > 0; - } - - pub fn eql(s: *const String, comptime _t: type, other: anytype) bool { - if (s.isUTF8()) { - switch (_t) { - @This() => { - if (other.isUTF8()) { - return strings.eqlLong(s.data, other.data, true); - } else { - return strings.utf16EqlString(other.slice16(), s.data); - } - }, - bun.string => { - return strings.eqlLong(s.data, other, true); - }, - []u16, []const u16 => { - return strings.utf16EqlString(other, s.data); - }, - else => { - @compileError("Invalid type"); - }, - } - } else { - switch (_t) { - @This() => { - if (other.isUTF8()) { - return strings.utf16EqlString(s.slice16(), other.data); - } else { - return std.mem.eql(u16, other.slice16(), s.slice16()); - } - }, - bun.string => { - return strings.utf16EqlString(s.slice16(), other); - }, - []u16, []const u16 => { - return std.mem.eql(u16, other.slice16(), s.slice16()); - }, - else => { - @compileError("Invalid type"); - }, - } - } - } - - pub fn eqlComptime(s: *const String, comptime value: []const u8) bool { - bun.assert(s.next == null); - return if (s.isUTF8()) - strings.eqlComptime(s.data, value) - else - strings.eqlComptimeUTF16(s.slice16(), value); - } - - pub fn hasPrefixComptime(s: *const String, comptime value: anytype) bool { - if (s.data.len < value.len) - return false; - - return if (s.isUTF8()) - strings.eqlComptime(s.data[0..value.len], value) - else - strings.eqlComptimeUTF16(s.slice16()[0..value.len], value); - } - - pub fn string(s: *const String, allocator: std.mem.Allocator) OOM!bun.string { - if (s.isUTF8()) { - return s.data; - } else { - return strings.toUTF8Alloc(allocator, s.slice16()); - } - } - - pub fn stringZ(s: *const String, allocator: std.mem.Allocator) OOM!bun.stringZ { - if (s.isUTF8()) { - return allocator.dupeZ(u8, s.data); - } else { - return strings.toUTF8AllocZ(allocator, s.slice16()); - } - } - - pub fn stringCloned(s: *const String, allocator: std.mem.Allocator) OOM!bun.string { - if (s.isUTF8()) { - return allocator.dupe(u8, s.data); - } else { - return strings.toUTF8Alloc(allocator, s.slice16()); - } - } - - pub fn hash(s: *const String) u64 { - if (s.isBlank()) return 0; - - if (s.isUTF8()) { - // hash utf-8 - return bun.hash(s.data); - } else { - // hash utf-16 - return bun.hash(@as([*]const u8, @ptrCast(s.slice16().ptr))[0 .. s.slice16().len * 2]); - } - } - - pub fn toJS(s: *String, allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) !JSC.JSValue { - s.resolveRopeIfNeeded(allocator); - if (!s.isPresent()) { - var emp = bun.String.empty; - return emp.toJS(globalObject); - } - - if (s.isUTF8()) { - if (try strings.toUTF16Alloc(allocator, s.slice8(), false, false)) |utf16| { - var out, const chars = bun.String.createUninitialized(.utf16, utf16.len); - @memcpy(chars, utf16); - return out.transferToJS(globalObject); - } else { - var out, const chars = bun.String.createUninitialized(.latin1, s.slice8().len); - @memcpy(chars, s.slice8()); - return out.transferToJS(globalObject); - } - } else { - var out, const chars = bun.String.createUninitialized(.utf16, s.slice16().len); - @memcpy(chars, s.slice16()); - return out.transferToJS(globalObject); - } - } - - pub fn toZigString(s: *String, allocator: std.mem.Allocator) JSC.ZigString { - if (s.isUTF8()) { - return JSC.ZigString.fromUTF8(s.slice(allocator)); - } else { - return JSC.ZigString.initUTF16(s.slice16()); - } - } - - pub fn format(s: String, comptime fmt: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - comptime bun.assert(fmt.len == 0); - - try writer.writeAll("E.String"); - if (s.next == null) { - try writer.writeAll("("); - if (s.isUTF8()) { - try writer.print("\"{s}\"", .{s.data}); - } else { - try writer.print("\"{}\"", .{bun.fmt.utf16(s.slice16())}); - } - try writer.writeAll(")"); - } else { - try writer.writeAll("(rope: ["); - var it: ?*const String = &s; - while (it) |part| { - if (part.isUTF8()) { - try writer.print("\"{s}\"", .{part.data}); - } else { - try writer.print("\"{}\"", .{bun.fmt.utf16(part.slice16())}); - } - it = part.next; - if (it != null) try writer.writeAll(" "); - } - try writer.writeAll("])"); - } - } - - pub fn jsonStringify(s: *const String, writer: anytype) !void { - var buf = [_]u8{0} ** 4096; - var i: usize = 0; - for (s.slice16()) |char| { - buf[i] = @as(u8, @intCast(char)); - i += 1; - if (i >= 4096) { - break; - } - } - - return try writer.write(buf[0..i]); - } - }; - - // value is in the Node - pub const TemplatePart = struct { - value: ExprNodeIndex, - tail_loc: logger.Loc, - tail: Template.Contents, - }; - - pub const Template = struct { - tag: ?ExprNodeIndex = null, - parts: []TemplatePart = &.{}, - head: Contents, - - pub const Contents = union(Tag) { - cooked: E.String, - raw: string, - - const Tag = enum { - cooked, - raw, - }; - - pub fn isUTF8(contents: Contents) bool { - return contents == .cooked and contents.cooked.isUTF8(); - } - }; - - /// "`a${'b'}c`" => "`abc`" - pub fn fold( - this: *Template, - allocator: std.mem.Allocator, - loc: logger.Loc, - ) Expr { - if (this.tag != null or (this.head == .cooked and !this.head.cooked.isUTF8())) { - // we only fold utf-8/ascii for now - return Expr{ - .data = .{ .e_template = this }, - .loc = loc, - }; - } - - bun.assert(this.head == .cooked); - - if (this.parts.len == 0) { - return Expr.init(E.String, this.head.cooked, loc); - } - - var parts = std.ArrayList(TemplatePart).initCapacity(allocator, this.parts.len) catch unreachable; - var head = Expr.init(E.String, this.head.cooked, loc); - for (this.parts) |part_src| { - var part = part_src; - bun.assert(part.tail == .cooked); - - part.value = part.value.unwrapInlined(); - - switch (part.value.data) { - .e_number => { - if (part.value.data.e_number.toString(allocator)) |s| { - part.value = Expr.init(E.String, E.String.init(s), part.value.loc); - } - }, - .e_null => { - part.value = Expr.init(E.String, E.String.init("null"), part.value.loc); - }, - .e_boolean => { - part.value = Expr.init(E.String, E.String.init(if (part.value.data.e_boolean.value) - "true" - else - "false"), part.value.loc); - }, - .e_undefined => { - part.value = Expr.init(E.String, E.String.init("undefined"), part.value.loc); - }, - .e_big_int => |value| { - part.value = Expr.init(E.String, E.String.init(value.value), part.value.loc); - }, - else => {}, - } - - if (part.value.data == .e_string and part.tail.cooked.isUTF8() and part.value.data.e_string.isUTF8()) { - if (parts.items.len == 0) { - if (part.value.data.e_string.len() > 0) { - head.data.e_string.push(Expr.init(E.String, part.value.data.e_string.*, logger.Loc.Empty).data.e_string); - } - - if (part.tail.cooked.len() > 0) { - head.data.e_string.push(Expr.init(E.String, part.tail.cooked, part.tail_loc).data.e_string); - } - - continue; - } else { - var prev_part = &parts.items[parts.items.len - 1]; - bun.assert(prev_part.tail == .cooked); - - if (prev_part.tail.cooked.isUTF8()) { - if (part.value.data.e_string.len() > 0) { - prev_part.tail.cooked.push(Expr.init(E.String, part.value.data.e_string.*, logger.Loc.Empty).data.e_string); - } - - if (part.tail.cooked.len() > 0) { - prev_part.tail.cooked.push(Expr.init(E.String, part.tail.cooked, part.tail_loc).data.e_string); - } - } else { - parts.appendAssumeCapacity(part); - } - } - } else { - parts.appendAssumeCapacity(part); - } - } - - if (parts.items.len == 0) { - parts.deinit(); - head.data.e_string.resolveRopeIfNeeded(allocator); - return head; - } - - return Expr.init(E.Template, .{ - .tag = null, - .parts = parts.items, - .head = .{ .cooked = head.data.e_string.* }, - }, loc); - } - }; - - pub const RegExp = struct { - value: string, - - // This exists for JavaScript bindings - // The RegExp constructor expects flags as a second argument. - // We want to avoid re-lexing the flags, so we store them here. - // This is the index of the first character in a flag, not the "/" - // /foo/gim - // ^ - flags_offset: ?u16 = null, - - pub var empty = RegExp{ .value = "" }; - - pub fn pattern(this: RegExp) string { - - // rewind until we reach the /foo/gim - // ^ - // should only ever be a single character - // but we're being cautious - if (this.flags_offset) |i_| { - var i = i_; - while (i > 0 and this.value[i] != '/') { - i -= 1; - } - - return std.mem.trim(u8, this.value[0..i], "/"); - } - - return std.mem.trim(u8, this.value, "/"); - } - - pub fn flags(this: RegExp) string { - // rewind until we reach the /foo/gim - // ^ - // should only ever be a single character - // but we're being cautious - if (this.flags_offset) |i| { - return this.value[i..]; - } - - return ""; - } - - pub fn jsonStringify(self: *const RegExp, writer: anytype) !void { - return try writer.write(self.value); - } - }; - - pub const Class = G.Class; - - pub const Await = struct { - value: ExprNodeIndex, - }; - - pub const Yield = struct { - value: ?ExprNodeIndex = null, - is_star: bool = false, - }; - - pub const If = struct { - test_: ExprNodeIndex, - yes: ExprNodeIndex, - no: ExprNodeIndex, - }; - - pub const RequireString = struct { - import_record_index: u32 = 0, - - unwrapped_id: u32 = std.math.maxInt(u32), - }; - - pub const RequireResolveString = struct { - import_record_index: u32, - - // close_paren_loc: logger.Loc = logger.Loc.Empty, - }; - - pub const InlinedEnum = struct { - value: ExprNodeIndex, - comment: string, - }; - - pub const Import = struct { - expr: ExprNodeIndex, - options: ExprNodeIndex = Expr.empty, - import_record_index: u32, - - /// TODO: - /// Comments inside "import()" expressions have special meaning for Webpack. - /// Preserving comments inside these expressions makes it possible to use - /// esbuild as a TypeScript-to-JavaScript frontend for Webpack to improve - /// performance. We intentionally do not interpret these comments in esbuild - /// because esbuild is not Webpack. But we do preserve them since doing so is - /// harmless, easy to maintain, and useful to people. See the Webpack docs for - /// more info: https://webpack.js.org/api/module-methods/#magic-comments. - // leading_interior_comments: []G.Comment = &([_]G.Comment{}), - - pub fn isImportRecordNull(this: *const Import) bool { - return this.import_record_index == std.math.maxInt(u32); - } - - pub fn importRecordLoader(import: *const Import) ?bun.options.Loader { - // This logic is duplicated in js_printer.zig fn parsePath() - const obj = import.options.data.as(.e_object) orelse - return null; - const with = obj.get("with") orelse obj.get("assert") orelse - return null; - const with_obj = with.data.as(.e_object) orelse - return null; - const str = (with_obj.get("type") orelse - return null).data.as(.e_string) orelse - return null; - - if (!str.is_utf16) if (bun.options.Loader.fromString(str.data)) |loader| { - if (loader == .sqlite) { - const embed = with_obj.get("embed") orelse return loader; - const embed_str = embed.data.as(.e_string) orelse return loader; - if (embed_str.eqlComptime("true")) { - return .sqlite_embedded; - } - } - return loader; - }; - - return null; - } - }; -}; - -pub const Stmt = struct { - loc: logger.Loc, - data: Data, - - pub const Batcher = NewBatcher(Stmt); - - pub fn assign(a: Expr, b: Expr) Stmt { - return Stmt.alloc( - S.SExpr, - S.SExpr{ - .value = Expr.assign(a, b), - }, - a.loc, - ); - } - - const Serializable = struct { - type: Tag, - object: string, - value: Data, - loc: logger.Loc, - }; - - pub fn jsonStringify(self: *const Stmt, writer: anytype) !void { - return try writer.write(Serializable{ .type = std.meta.activeTag(self.data), .object = "stmt", .value = self.data, .loc = self.loc }); - } - - pub fn isTypeScript(self: *Stmt) bool { - return @as(Stmt.Tag, self.data) == .s_type_script; - } - - pub fn isSuperCall(self: Stmt) bool { - return self.data == .s_expr and self.data.s_expr.value.data == .e_call and self.data.s_expr.value.data.e_call.target.data == .e_super; - } - - pub fn isMissingExpr(self: Stmt) bool { - return self.data == .s_expr and self.data.s_expr.value.data == .e_missing; - } - - pub fn empty() Stmt { - return Stmt{ .data = .{ .s_empty = None }, .loc = logger.Loc{} }; - } - - pub fn toEmpty(this: Stmt) Stmt { - return .{ - .data = .{ - .s_empty = None, - }, - .loc = this.loc, - }; - } - - const None = S.Empty{}; - - pub var icount: usize = 0; - pub fn init(comptime StatementType: type, origData: *StatementType, loc: logger.Loc) Stmt { - icount += 1; - - return switch (comptime StatementType) { - S.Empty => Stmt{ .loc = loc, .data = Data{ .s_empty = S.Empty{} } }, - S.Block => Stmt.comptime_init("s_block", S.Block, origData, loc), - S.Break => Stmt.comptime_init("s_break", S.Break, origData, loc), - S.Class => Stmt.comptime_init("s_class", S.Class, origData, loc), - S.Comment => Stmt.comptime_init("s_comment", S.Comment, origData, loc), - S.Continue => Stmt.comptime_init("s_continue", S.Continue, origData, loc), - S.Debugger => Stmt.comptime_init("s_debugger", S.Debugger, origData, loc), - S.Directive => Stmt.comptime_init("s_directive", S.Directive, origData, loc), - S.DoWhile => Stmt.comptime_init("s_do_while", S.DoWhile, origData, loc), - S.Enum => Stmt.comptime_init("s_enum", S.Enum, origData, loc), - S.ExportClause => Stmt.comptime_init("s_export_clause", S.ExportClause, origData, loc), - S.ExportDefault => Stmt.comptime_init("s_export_default", S.ExportDefault, origData, loc), - S.ExportEquals => Stmt.comptime_init("s_export_equals", S.ExportEquals, origData, loc), - S.ExportFrom => Stmt.comptime_init("s_export_from", S.ExportFrom, origData, loc), - S.ExportStar => Stmt.comptime_init("s_export_star", S.ExportStar, origData, loc), - S.SExpr => Stmt.comptime_init("s_expr", S.SExpr, origData, loc), - S.ForIn => Stmt.comptime_init("s_for_in", S.ForIn, origData, loc), - S.ForOf => Stmt.comptime_init("s_for_of", S.ForOf, origData, loc), - S.For => Stmt.comptime_init("s_for", S.For, origData, loc), - S.Function => Stmt.comptime_init("s_function", S.Function, origData, loc), - S.If => Stmt.comptime_init("s_if", S.If, origData, loc), - S.Import => Stmt.comptime_init("s_import", S.Import, origData, loc), - S.Label => Stmt.comptime_init("s_label", S.Label, origData, loc), - S.Local => Stmt.comptime_init("s_local", S.Local, origData, loc), - S.Namespace => Stmt.comptime_init("s_namespace", S.Namespace, origData, loc), - S.Return => Stmt.comptime_init("s_return", S.Return, origData, loc), - S.Switch => Stmt.comptime_init("s_switch", S.Switch, origData, loc), - S.Throw => Stmt.comptime_init("s_throw", S.Throw, origData, loc), - S.Try => Stmt.comptime_init("s_try", S.Try, origData, loc), - S.TypeScript => Stmt.comptime_init("s_type_script", S.TypeScript, origData, loc), - S.While => Stmt.comptime_init("s_while", S.While, origData, loc), - S.With => Stmt.comptime_init("s_with", S.With, origData, loc), - else => @compileError("Invalid type in Stmt.init"), - }; - } - inline fn comptime_alloc(comptime tag_name: string, comptime typename: type, origData: anytype, loc: logger.Loc) Stmt { - return Stmt{ - .loc = loc, - .data = @unionInit( - Data, - tag_name, - Data.Store.append( - typename, - origData, - ), - ), - }; - } - - fn allocateData(allocator: std.mem.Allocator, comptime tag_name: string, comptime typename: type, origData: anytype, loc: logger.Loc) Stmt { - const value = allocator.create(@TypeOf(origData)) catch unreachable; - value.* = origData; - - return comptime_init(tag_name, *typename, value, loc); - } - - inline fn comptime_init(comptime tag_name: string, comptime TypeName: type, origData: TypeName, loc: logger.Loc) Stmt { - return Stmt{ .loc = loc, .data = @unionInit(Data, tag_name, origData) }; - } - - pub fn alloc(comptime StatementData: type, origData: StatementData, loc: logger.Loc) Stmt { - Stmt.Data.Store.assert(); - - icount += 1; - return switch (StatementData) { - S.Block => Stmt.comptime_alloc("s_block", S.Block, origData, loc), - S.Break => Stmt.comptime_alloc("s_break", S.Break, origData, loc), - S.Class => Stmt.comptime_alloc("s_class", S.Class, origData, loc), - S.Comment => Stmt.comptime_alloc("s_comment", S.Comment, origData, loc), - S.Continue => Stmt.comptime_alloc("s_continue", S.Continue, origData, loc), - S.Debugger => Stmt{ .loc = loc, .data = .{ .s_debugger = origData } }, - S.Directive => Stmt.comptime_alloc("s_directive", S.Directive, origData, loc), - S.DoWhile => Stmt.comptime_alloc("s_do_while", S.DoWhile, origData, loc), - S.Empty => Stmt{ .loc = loc, .data = Data{ .s_empty = S.Empty{} } }, - S.Enum => Stmt.comptime_alloc("s_enum", S.Enum, origData, loc), - S.ExportClause => Stmt.comptime_alloc("s_export_clause", S.ExportClause, origData, loc), - S.ExportDefault => Stmt.comptime_alloc("s_export_default", S.ExportDefault, origData, loc), - S.ExportEquals => Stmt.comptime_alloc("s_export_equals", S.ExportEquals, origData, loc), - S.ExportFrom => Stmt.comptime_alloc("s_export_from", S.ExportFrom, origData, loc), - S.ExportStar => Stmt.comptime_alloc("s_export_star", S.ExportStar, origData, loc), - S.SExpr => Stmt.comptime_alloc("s_expr", S.SExpr, origData, loc), - S.ForIn => Stmt.comptime_alloc("s_for_in", S.ForIn, origData, loc), - S.ForOf => Stmt.comptime_alloc("s_for_of", S.ForOf, origData, loc), - S.For => Stmt.comptime_alloc("s_for", S.For, origData, loc), - S.Function => Stmt.comptime_alloc("s_function", S.Function, origData, loc), - S.If => Stmt.comptime_alloc("s_if", S.If, origData, loc), - S.Import => Stmt.comptime_alloc("s_import", S.Import, origData, loc), - S.Label => Stmt.comptime_alloc("s_label", S.Label, origData, loc), - S.Local => Stmt.comptime_alloc("s_local", S.Local, origData, loc), - S.Namespace => Stmt.comptime_alloc("s_namespace", S.Namespace, origData, loc), - S.Return => Stmt.comptime_alloc("s_return", S.Return, origData, loc), - S.Switch => Stmt.comptime_alloc("s_switch", S.Switch, origData, loc), - S.Throw => Stmt.comptime_alloc("s_throw", S.Throw, origData, loc), - S.Try => Stmt.comptime_alloc("s_try", S.Try, origData, loc), - S.TypeScript => Stmt{ .loc = loc, .data = Data{ .s_type_script = S.TypeScript{} } }, - S.While => Stmt.comptime_alloc("s_while", S.While, origData, loc), - S.With => Stmt.comptime_alloc("s_with", S.With, origData, loc), - else => @compileError("Invalid type in Stmt.init"), - }; - } - - pub const Disabler = bun.DebugOnlyDisabler(@This()); - - /// When the lifetime of an Stmt.Data's pointer must exist longer than reset() is called, use this function. - /// Be careful to free the memory (or use an allocator that does it for you) - /// Also, prefer Stmt.init or Stmt.alloc when possible. This will be slower. - pub fn allocate(allocator: std.mem.Allocator, comptime StatementData: type, origData: StatementData, loc: logger.Loc) Stmt { - Stmt.Data.Store.assert(); - - icount += 1; - return switch (StatementData) { - S.Block => Stmt.allocateData(allocator, "s_block", S.Block, origData, loc), - S.Break => Stmt.allocateData(allocator, "s_break", S.Break, origData, loc), - S.Class => Stmt.allocateData(allocator, "s_class", S.Class, origData, loc), - S.Comment => Stmt.allocateData(allocator, "s_comment", S.Comment, origData, loc), - S.Continue => Stmt.allocateData(allocator, "s_continue", S.Continue, origData, loc), - S.Debugger => Stmt{ .loc = loc, .data = .{ .s_debugger = origData } }, - S.Directive => Stmt.allocateData(allocator, "s_directive", S.Directive, origData, loc), - S.DoWhile => Stmt.allocateData(allocator, "s_do_while", S.DoWhile, origData, loc), - S.Empty => Stmt{ .loc = loc, .data = Data{ .s_empty = S.Empty{} } }, - S.Enum => Stmt.allocateData(allocator, "s_enum", S.Enum, origData, loc), - S.ExportClause => Stmt.allocateData(allocator, "s_export_clause", S.ExportClause, origData, loc), - S.ExportDefault => Stmt.allocateData(allocator, "s_export_default", S.ExportDefault, origData, loc), - S.ExportEquals => Stmt.allocateData(allocator, "s_export_equals", S.ExportEquals, origData, loc), - S.ExportFrom => Stmt.allocateData(allocator, "s_export_from", S.ExportFrom, origData, loc), - S.ExportStar => Stmt.allocateData(allocator, "s_export_star", S.ExportStar, origData, loc), - S.SExpr => Stmt.allocateData(allocator, "s_expr", S.SExpr, origData, loc), - S.ForIn => Stmt.allocateData(allocator, "s_for_in", S.ForIn, origData, loc), - S.ForOf => Stmt.allocateData(allocator, "s_for_of", S.ForOf, origData, loc), - S.For => Stmt.allocateData(allocator, "s_for", S.For, origData, loc), - S.Function => Stmt.allocateData(allocator, "s_function", S.Function, origData, loc), - S.If => Stmt.allocateData(allocator, "s_if", S.If, origData, loc), - S.Import => Stmt.allocateData(allocator, "s_import", S.Import, origData, loc), - S.Label => Stmt.allocateData(allocator, "s_label", S.Label, origData, loc), - S.Local => Stmt.allocateData(allocator, "s_local", S.Local, origData, loc), - S.Namespace => Stmt.allocateData(allocator, "s_namespace", S.Namespace, origData, loc), - S.Return => Stmt.allocateData(allocator, "s_return", S.Return, origData, loc), - S.Switch => Stmt.allocateData(allocator, "s_switch", S.Switch, origData, loc), - S.Throw => Stmt.allocateData(allocator, "s_throw", S.Throw, origData, loc), - S.Try => Stmt.allocateData(allocator, "s_try", S.Try, origData, loc), - S.TypeScript => Stmt{ .loc = loc, .data = Data{ .s_type_script = S.TypeScript{} } }, - S.While => Stmt.allocateData(allocator, "s_while", S.While, origData, loc), - S.With => Stmt.allocateData(allocator, "s_with", S.With, origData, loc), - else => @compileError("Invalid type in Stmt.init"), - }; - } - - pub fn allocateExpr(allocator: std.mem.Allocator, expr: Expr) Stmt { - return Stmt.allocate(allocator, S.SExpr, S.SExpr{ .value = expr }, expr.loc); - } - - pub const Tag = enum { - s_block, - s_break, - s_class, - s_comment, - s_continue, - s_directive, - s_do_while, - s_enum, - s_export_clause, - s_export_default, - s_export_equals, - s_export_from, - s_export_star, - s_expr, - s_for_in, - s_for_of, - s_for, - s_function, - s_if, - s_import, - s_label, - s_local, - s_namespace, - s_return, - s_switch, - s_throw, - s_try, - s_while, - s_with, - s_type_script, - s_empty, - s_debugger, - s_lazy_export, - - pub fn jsonStringify(self: @This(), writer: anytype) !void { - return try writer.write(@tagName(self)); - } - - pub fn isExportLike(tag: Tag) bool { - return switch (tag) { - .s_export_clause, .s_export_default, .s_export_equals, .s_export_from, .s_export_star, .s_empty => true, - else => false, - }; - } - }; - - pub const Data = union(Tag) { - s_block: *S.Block, - s_break: *S.Break, - s_class: *S.Class, - s_comment: *S.Comment, - s_continue: *S.Continue, - s_directive: *S.Directive, - s_do_while: *S.DoWhile, - s_enum: *S.Enum, - s_export_clause: *S.ExportClause, - s_export_default: *S.ExportDefault, - s_export_equals: *S.ExportEquals, - s_export_from: *S.ExportFrom, - s_export_star: *S.ExportStar, - s_expr: *S.SExpr, - s_for_in: *S.ForIn, - s_for_of: *S.ForOf, - s_for: *S.For, - s_function: *S.Function, - s_if: *S.If, - s_import: *S.Import, - s_label: *S.Label, - s_local: *S.Local, - s_namespace: *S.Namespace, - s_return: *S.Return, - s_switch: *S.Switch, - s_throw: *S.Throw, - s_try: *S.Try, - s_while: *S.While, - s_with: *S.With, - - s_type_script: S.TypeScript, - s_empty: S.Empty, // special case, its a zero value type - s_debugger: S.Debugger, - - s_lazy_export: *Expr.Data, - - comptime { - if (@sizeOf(Stmt) > 24) { - @compileLog("Expected Stmt to be <= 24 bytes, but it is", @sizeOf(Stmt), " bytes"); - } - } - - pub const Store = struct { - const StoreType = NewStore(&.{ - S.Block, - S.Break, - S.Class, - S.Comment, - S.Continue, - S.Directive, - S.DoWhile, - S.Enum, - S.ExportClause, - S.ExportDefault, - S.ExportEquals, - S.ExportFrom, - S.ExportStar, - S.SExpr, - S.ForIn, - S.ForOf, - S.For, - S.Function, - S.If, - S.Import, - S.Label, - S.Local, - S.Namespace, - S.Return, - S.Switch, - S.Throw, - S.Try, - S.While, - S.With, - }, 128); - - pub threadlocal var instance: ?*StoreType = null; - pub threadlocal var memory_allocator: ?*ASTMemoryAllocator = null; - pub threadlocal var disable_reset = false; - - pub fn create() void { - if (instance != null or memory_allocator != null) { - return; - } - - instance = StoreType.init(); - } - - /// create || reset - pub fn begin() void { - if (memory_allocator != null) return; - if (instance == null) { - create(); - return; - } - - if (!disable_reset) - instance.?.reset(); - } - - pub fn reset() void { - if (disable_reset or memory_allocator != null) return; - instance.?.reset(); - } - - pub fn deinit() void { - if (instance == null or memory_allocator != null) return; - instance.?.deinit(); - instance = null; - } - - pub inline fn assert() void { - if (comptime Environment.allow_assert) { - if (instance == null and memory_allocator == null) - bun.unreachablePanic("Store must be init'd", .{}); - } - } - - pub fn append(comptime T: type, value: T) *T { - if (memory_allocator) |allocator| { - return allocator.append(T, value); - } - - Disabler.assert(); - return instance.?.append(T, value); - } - }; - }; - - pub fn StoredData(tag: Tag) type { - const T = @FieldType(Data, tag); - return switch (@typeInfo(T)) { - .pointer => |ptr| ptr.child, - else => T, - }; - } - - pub fn caresAboutScope(self: *Stmt) bool { - return switch (self.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 => |local| { - return local.kind != .k_var; - }, - else => { - return true; - }, - }; - } -}; - -pub const Expr = struct { - loc: logger.Loc, - data: Data, - - pub const empty = Expr{ .data = .{ .e_missing = E.Missing{} }, .loc = logger.Loc.Empty }; - - pub fn isAnonymousNamed(expr: Expr) bool { - return switch (expr.data) { - .e_arrow => true, - .e_function => |func| func.func.name == null, - .e_class => |class| class.class_name == null, - else => false, - }; - } - - pub fn clone(this: Expr, allocator: std.mem.Allocator) !Expr { - return .{ - .loc = this.loc, - .data = try this.data.clone(allocator), - }; - } - - pub fn deepClone(this: Expr, allocator: std.mem.Allocator) anyerror!Expr { - return .{ - .loc = this.loc, - .data = try this.data.deepClone(allocator), - }; - } - - pub fn wrapInArrow(this: Expr, allocator: std.mem.Allocator) !Expr { - var stmts = try allocator.alloc(Stmt, 1); - stmts[0] = Stmt.alloc(S.Return, S.Return{ .value = this }, this.loc); - - return Expr.init(E.Arrow, E.Arrow{ - .args = &.{}, - .body = .{ - .loc = this.loc, - .stmts = stmts, - }, - }, this.loc); - } - - pub fn canBeInlinedFromPropertyAccess(this: Expr) bool { - return switch (this.data) { - // if the array has a spread we must keep it - // https://github.com/oven-sh/bun/issues/2594 - .e_spread => false, - - .e_missing => false, - else => true, - }; - } - - pub fn canBeConstValue(this: Expr) bool { - return this.data.canBeConstValue(); - } - - pub fn canBeMoved(expr: Expr) bool { - return expr.data.canBeMoved(); - } - - pub fn unwrapInlined(expr: Expr) Expr { - if (expr.data.as(.e_inlined_enum)) |inlined| return inlined.value; - return expr; - } - - pub fn fromBlob( - blob: *const JSC.WebCore.Blob, - allocator: std.mem.Allocator, - mime_type_: ?MimeType, - log: *logger.Log, - loc: logger.Loc, - ) !Expr { - const bytes = blob.sharedView(); - - const mime_type = mime_type_ orelse MimeType.init(blob.content_type, null, null); - - if (mime_type.category == .json) { - const source = &logger.Source.initPathString("fetch.json", bytes); - var out_expr = JSONParser.parseForMacro(source, log, allocator) catch { - return error.MacroFailed; - }; - out_expr.loc = loc; - - 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()) { - var output = MutableString.initEmpty(allocator); - output = try JSPrinter.quoteForJSON(bytes, output, true); - var list = output.toOwnedSlice(); - // remove the quotes - if (list.len > 0) { - list = list[1 .. list.len - 1]; - } - return Expr.init(E.String, E.String.init(list), loc); - } - - return Expr.init( - E.String, - E.String{ - .data = try JSC.ZigString.init(bytes).toBase64DataURL(allocator), - }, - loc, - ); - } - - pub inline fn initIdentifier(ref: Ref, loc: logger.Loc) Expr { - return Expr{ - .loc = loc, - .data = .{ - .e_identifier = E.Identifier.init(ref), - }, - }; - } - - pub fn toEmpty(expr: Expr) Expr { - return Expr{ .data = .{ .e_missing = E.Missing{} }, .loc = expr.loc }; - } - pub fn isEmpty(expr: Expr) bool { - return expr.data == .e_missing; - } - pub const Query = struct { expr: Expr, loc: logger.Loc, i: u32 = 0 }; - - pub fn hasAnyPropertyNamed(expr: *const Expr, comptime names: []const string) bool { - if (std.meta.activeTag(expr.data) != .e_object) return false; - const obj = expr.data.e_object; - if (obj.properties.len == 0) return false; - - for (obj.properties.slice()) |prop| { - if (prop.value == null) continue; - const key = prop.key orelse continue; - if (std.meta.activeTag(key.data) != .e_string) continue; - const key_str = key.data.e_string; - if (strings.eqlAnyComptime(key_str.data, names)) return true; - } - - return false; - } - - pub fn toJS(this: Expr, allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) ToJSError!JSC.JSValue { - return this.data.toJS(allocator, globalObject); - } - - pub inline fn isArray(this: *const Expr) bool { - return this.data == .e_array; - } - - pub inline fn isObject(this: *const Expr) bool { - return this.data == .e_object; - } - - pub fn get(expr: *const Expr, name: string) ?Expr { - return if (asProperty(expr, name)) |query| query.expr else null; - } - - /// Only use this for pretty-printing JSON. Do not use in transpiler. - /// - /// This does not handle edgecases like `-1` or stringifying arbitrary property lookups. - pub fn getByIndex(expr: *const Expr, index: u32, index_str: string, allocator: std.mem.Allocator) ?Expr { - switch (expr.data) { - .e_array => |array| { - if (index >= array.items.len) return null; - return array.items.slice()[index]; - }, - .e_object => |object| { - for (object.properties.sliceConst()) |*prop| { - const key = &(prop.key orelse continue); - switch (key.data) { - .e_string => |str| { - if (str.eql(string, index_str)) { - return prop.value; - } - }, - .e_number => |num| { - if (num.toU32() == index) { - return prop.value; - } - }, - else => {}, - } - } - - return null; - }, - .e_string => |str| { - if (str.len() > index) { - var slice = str.slice(allocator); - // TODO: this is not correct since .length refers to UTF-16 code units and not UTF-8 bytes - // However, since this is only used in the JSON prettifier for `bun pm view`, it's not a blocker for shipping. - if (slice.len > index) { - return Expr.init(E.String, .{ .data = slice[index..][0..1] }, expr.loc); - } - } - }, - else => {}, - } - - return null; - } - - /// This supports lookups like: - /// - `foo` - /// - `foo.bar` - /// - `foo[123]` - /// - `foo[123].bar` - /// - `foo[123].bar[456]` - /// - `foo[123].bar[456].baz` - /// - `foo[123].bar[456].baz.qux` // etc. - /// - /// This is not intended for use by the transpiler, instead by pretty printing JSON. - pub fn getPathMayBeIndex(expr: *const Expr, name: string) ?Expr { - if (name.len == 0) { - return null; - } - - if (strings.indexOfAny(name, "[.")) |idx| { - switch (name[idx]) { - '[' => { - const end_idx = strings.indexOfChar(name, ']') orelse return null; - var base_expr = expr; - if (idx > 0) { - const key = name[0..idx]; - base_expr = &(base_expr.get(key) orelse return null); - } - - const index_str = name[idx + 1 .. end_idx]; - const index = std.fmt.parseInt(u32, index_str, 10) catch return null; - const rest = if (name.len > end_idx) name[end_idx + 1 ..] else ""; - const result = &(base_expr.getByIndex(index, index_str, bun.default_allocator) orelse return null); - if (rest.len > 0) return result.getPathMayBeIndex(rest); - return result.*; - }, - '.' => { - const key = name[0..idx]; - const sub_expr = &(expr.get(key) orelse return null); - const subpath = if (name.len > idx) name[idx + 1 ..] else ""; - if (subpath.len > 0) { - return sub_expr.getPathMayBeIndex(subpath); - } - - return sub_expr.*; - }, - else => unreachable, - } - } - - return expr.get(name); - } - - /// Don't use this if you care about performance. - /// - /// Sets the value of a property, creating it if it doesn't exist. - /// `expr` must be an object. - pub fn set(expr: *Expr, allocator: std.mem.Allocator, name: string, value: Expr) OOM!void { - bun.assertWithLocation(expr.isObject(), @src()); - for (0..expr.data.e_object.properties.len) |i| { - const prop = &expr.data.e_object.properties.ptr[i]; - const key = prop.key orelse continue; - if (std.meta.activeTag(key.data) != .e_string) continue; - if (key.data.e_string.eql(string, name)) { - prop.value = value; - return; - } - } - - var new_props = expr.data.e_object.properties.listManaged(allocator); - try new_props.append(.{ - .key = Expr.init(E.String, .{ .data = name }, logger.Loc.Empty), - .value = value, - }); - - expr.data.e_object.properties = BabyList(G.Property).fromList(new_props); - } - - /// Don't use this if you care about performance. - /// - /// Sets the value of a property to a string, creating it if it doesn't exist. - /// `expr` must be an object. - pub fn setString(expr: *Expr, allocator: std.mem.Allocator, name: string, value: string) OOM!void { - bun.assertWithLocation(expr.isObject(), @src()); - for (0..expr.data.e_object.properties.len) |i| { - const prop = &expr.data.e_object.properties.ptr[i]; - const key = prop.key orelse continue; - if (std.meta.activeTag(key.data) != .e_string) continue; - if (key.data.e_string.eql(string, name)) { - prop.value = Expr.init(E.String, .{ .data = value }, logger.Loc.Empty); - return; - } - } - - var new_props = expr.data.e_object.properties.listManaged(allocator); - try new_props.append(.{ - .key = Expr.init(E.String, .{ .data = name }, logger.Loc.Empty), - .value = Expr.init(E.String, .{ .data = value }, logger.Loc.Empty), - }); - - expr.data.e_object.properties = BabyList(G.Property).fromList(new_props); - } - - pub fn getObject(expr: *const Expr, name: string) ?Expr { - if (expr.asProperty(name)) |query| { - if (query.expr.isObject()) { - return query.expr; - } - } - return null; - } - - pub fn getString(expr: *const Expr, allocator: std.mem.Allocator, name: string) OOM!?struct { string, logger.Loc } { - if (asProperty(expr, name)) |q| { - if (q.expr.asString(allocator)) |str| { - return .{ - str, - q.expr.loc, - }; - } - } - return null; - } - - pub fn getNumber(expr: *const Expr, name: string) ?struct { f64, logger.Loc } { - if (asProperty(expr, name)) |q| { - if (q.expr.asNumber()) |num| { - return .{ - num, - q.expr.loc, - }; - } - } - return null; - } - - pub fn getStringCloned(expr: *const Expr, allocator: std.mem.Allocator, name: string) OOM!?string { - return if (asProperty(expr, name)) |q| q.expr.asStringCloned(allocator) else null; - } - - pub fn getStringClonedZ(expr: *const Expr, allocator: std.mem.Allocator, name: string) OOM!?stringZ { - return if (asProperty(expr, name)) |q| q.expr.asStringZ(allocator) else null; - } - - pub fn getArray(expr: *const Expr, name: string) ?ArrayIterator { - return if (asProperty(expr, name)) |q| q.expr.asArray() else null; - } - - pub fn getRope(self: *const Expr, rope: *const E.Object.Rope) ?E.Object.RopeQuery { - if (self.get(rope.head.data.e_string.data)) |existing| { - switch (existing.data) { - .e_array => |array| { - if (rope.next) |next| { - if (array.items.last()) |end| { - return end.getRope(next); - } - } - - return E.Object.RopeQuery{ - .expr = existing, - .rope = rope, - }; - }, - .e_object => { - if (rope.next) |next| { - if (existing.getRope(next)) |end| { - return end; - } - } - - return E.Object.RopeQuery{ - .expr = existing, - .rope = rope, - }; - }, - else => return E.Object.RopeQuery{ - .expr = existing, - .rope = rope, - }, - } - } - - return null; - } - - // Making this comptime bloats the binary and doesn't seem to impact runtime performance. - pub fn asProperty(expr: *const Expr, name: string) ?Query { - if (std.meta.activeTag(expr.data) != .e_object) return null; - const obj = expr.data.e_object; - if (obj.properties.len == 0) return null; - - return obj.asProperty(name); - } - - pub fn asPropertyStringMap(expr: *const Expr, name: string, allocator: std.mem.Allocator) ?*bun.StringArrayHashMap(string) { - if (std.meta.activeTag(expr.data) != .e_object) return null; - const obj_ = expr.data.e_object; - if (obj_.properties.len == 0) return null; - const query = obj_.asProperty(name) orelse return null; - if (query.expr.data != .e_object) return null; - - const obj = query.expr.data.e_object; - var count: usize = 0; - for (obj.properties.slice()) |prop| { - const key = prop.key.?.asString(allocator) orelse continue; - const value = prop.value.?.asString(allocator) orelse continue; - count += @as(usize, @intFromBool(key.len > 0 and value.len > 0)); - } - - if (count == 0) return null; - var map = bun.StringArrayHashMap(string).init(allocator); - map.ensureUnusedCapacity(count) catch return null; - - for (obj.properties.slice()) |prop| { - const key = prop.key.?.asString(allocator) orelse continue; - const value = prop.value.?.asString(allocator) orelse continue; - - if (!(key.len > 0 and value.len > 0)) continue; - - map.putAssumeCapacity(key, value); - } - - const ptr = allocator.create(bun.StringArrayHashMap(string)) catch unreachable; - ptr.* = map; - return ptr; - } - - pub const ArrayIterator = struct { - array: *const E.Array, - index: u32, - - pub fn next(this: *ArrayIterator) ?Expr { - if (this.index >= this.array.items.len) { - return null; - } - defer this.index += 1; - return this.array.items.ptr[this.index]; - } - }; - - pub fn asArray(expr: *const Expr) ?ArrayIterator { - if (std.meta.activeTag(expr.data) != .e_array) return null; - const array = expr.data.e_array; - if (array.items.len == 0) return null; - - return ArrayIterator{ .array = array, .index = 0 }; - } - - pub inline fn asUtf8StringLiteral(expr: *const Expr) ?string { - if (expr.data == .e_string) { - bun.debugAssert(expr.data.e_string.next == null); - return expr.data.e_string.data; - } - return null; - } - - pub inline fn asStringLiteral(expr: *const Expr, allocator: std.mem.Allocator) ?string { - if (std.meta.activeTag(expr.data) != .e_string) return null; - return expr.data.e_string.string(allocator) catch null; - } - - pub inline fn isString(expr: *const Expr) bool { - return switch (expr.data) { - .e_string => true, - else => false, - }; - } - - pub inline fn asString(expr: *const Expr, allocator: std.mem.Allocator) ?string { - switch (expr.data) { - .e_string => |str| return str.string(allocator) catch bun.outOfMemory(), - else => return null, - } - } - pub inline fn asStringHash(expr: *const Expr, allocator: std.mem.Allocator, comptime hash_fn: *const fn (buf: []const u8) callconv(.Inline) u64) OOM!?u64 { - switch (expr.data) { - .e_string => |str| { - if (str.isUTF8()) return hash_fn(str.data); - const utf8_str = try str.string(allocator); - defer allocator.free(utf8_str); - return hash_fn(utf8_str); - }, - else => return null, - } - } - - pub inline fn asStringCloned(expr: *const Expr, allocator: std.mem.Allocator) OOM!?string { - switch (expr.data) { - .e_string => |str| return try str.stringCloned(allocator), - else => return null, - } - } - - pub inline fn asStringZ(expr: *const Expr, allocator: std.mem.Allocator) OOM!?stringZ { - switch (expr.data) { - .e_string => |str| return try str.stringZ(allocator), - else => return null, - } - } - - pub fn asBool( - expr: *const Expr, - ) ?bool { - if (std.meta.activeTag(expr.data) != .e_boolean) return null; - - return expr.data.e_boolean.value; - } - - pub fn asNumber(expr: *const Expr) ?f64 { - if (expr.data != .e_number) return null; - - return expr.data.e_number.value; - } - - pub const EFlags = enum { none, ts_decorator }; - - const Serializable = struct { - type: Tag, - object: string, - value: Data, - loc: logger.Loc, - }; - - pub fn isMissing(a: *const Expr) bool { - return std.meta.activeTag(a.data) == Expr.Tag.e_missing; - } - - // The goal of this function is to "rotate" the AST if it's possible to use the - // left-associative property of the operator to avoid unnecessary parentheses. - // - // When using this, make absolutely sure that the operator is actually - // associative. For example, the "-" operator is not associative for - // floating-point numbers. - pub fn joinWithLeftAssociativeOp( - comptime op: Op.Code, - a: Expr, - b: Expr, - allocator: std.mem.Allocator, - ) Expr { - // "(a, b) op c" => "a, b op c" - switch (a.data) { - .e_binary => |comma| { - if (comma.op == .bin_comma) { - comma.right = joinWithLeftAssociativeOp(op, comma.right, b, allocator); - } - }, - else => {}, - } - - // "a op (b op c)" => "(a op b) op c" - // "a op (b op (c op d))" => "((a op b) op c) op d" - switch (b.data) { - .e_binary => |binary| { - if (binary.op == op) { - return joinWithLeftAssociativeOp( - op, - joinWithLeftAssociativeOp(op, a, binary.left, allocator), - binary.right, - allocator, - ); - } - }, - else => {}, - } - - // "a op b" => "a op b" - // "(a op b) op c" => "(a op b) op c" - return Expr.init(E.Binary, E.Binary{ .op = op, .left = a, .right = b }, a.loc); - } - - pub fn joinWithComma(a: Expr, b: Expr, _: std.mem.Allocator) Expr { - if (a.isMissing()) { - return b; - } - - if (b.isMissing()) { - return a; - } - - return Expr.init(E.Binary, E.Binary{ .op = .bin_comma, .left = a, .right = b }, a.loc); - } - - pub fn joinAllWithComma(all: []Expr, allocator: std.mem.Allocator) Expr { - bun.assert(all.len > 0); - switch (all.len) { - 1 => { - return all[0]; - }, - 2 => { - return Expr.joinWithComma(all[0], all[1], allocator); - }, - else => { - var expr = all[0]; - for (1..all.len) |i| { - expr = Expr.joinWithComma(expr, all[i], allocator); - } - return expr; - }, - } - } - - pub fn joinAllWithCommaCallback(all: []Expr, comptime Context: type, ctx: Context, comptime callback: (fn (ctx: anytype, expr: Expr) ?Expr), allocator: std.mem.Allocator) ?Expr { - switch (all.len) { - 0 => return null, - 1 => { - return callback(ctx, all[0]); - }, - 2 => { - return Expr.joinWithComma( - callback(ctx, all[0]) orelse Expr{ - .data = .{ .e_missing = .{} }, - .loc = all[0].loc, - }, - callback(ctx, all[1]) orelse Expr{ - .data = .{ .e_missing = .{} }, - .loc = all[1].loc, - }, - allocator, - ); - }, - else => { - var i: usize = 1; - var expr = callback(ctx, all[0]) orelse Expr{ - .data = .{ .e_missing = .{} }, - .loc = all[0].loc, - }; - - while (i < all.len) : (i += 1) { - expr = Expr.joinWithComma(expr, callback(ctx, all[i]) orelse Expr{ - .data = .{ .e_missing = .{} }, - .loc = all[i].loc, - }, allocator); - } - - return expr; - }, - } - } - - pub fn jsonStringify(self: *const @This(), writer: anytype) !void { - return try writer.write(Serializable{ .type = std.meta.activeTag(self.data), .object = "expr", .value = self.data, .loc = self.loc }); - } - - pub fn extractNumericValues(left: Expr.Data, right: Expr.Data) ?[2]f64 { - return .{ - left.extractNumericValue() orelse return null, - right.extractNumericValue() orelse return null, - }; - } - - pub var icount: usize = 0; - - // We don't need to dynamically allocate booleans - var true_bool = E.Boolean{ .value = true }; - var false_bool = E.Boolean{ .value = false }; - var bool_values = [_]*E.Boolean{ &false_bool, &true_bool }; - - /// When the lifetime of an Expr.Data's pointer must exist longer than reset() is called, use this function. - /// Be careful to free the memory (or use an allocator that does it for you) - /// Also, prefer Expr.init or Expr.alloc when possible. This will be slower. - pub fn allocate(allocator: std.mem.Allocator, comptime Type: type, st: Type, loc: logger.Loc) Expr { - icount += 1; - Data.Store.assert(); - - switch (Type) { - E.Array => { - return Expr{ - .loc = loc, - .data = Data{ - .e_array = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.Class => { - return Expr{ - .loc = loc, - .data = Data{ - .e_class = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.Unary => { - return Expr{ - .loc = loc, - .data = Data{ - .e_unary = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.Binary => { - return Expr{ - .loc = loc, - .data = Data{ - .e_binary = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.This => { - return Expr{ - .loc = loc, - .data = Data{ - .e_this = st, - }, - }; - }, - E.Boolean => { - return Expr{ - .loc = loc, - .data = Data{ - .e_boolean = st, - }, - }; - }, - E.Super => { - return Expr{ - .loc = loc, - .data = Data{ - .e_super = st, - }, - }; - }, - E.Null => { - return Expr{ - .loc = loc, - .data = Data{ - .e_null = st, - }, - }; - }, - E.Undefined => { - return Expr{ - .loc = loc, - .data = Data{ - .e_undefined = st, - }, - }; - }, - E.New => { - return Expr{ - .loc = loc, - .data = Data{ - .e_new = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.NewTarget => { - return Expr{ - .loc = loc, - .data = Data{ - .e_new_target = st, - }, - }; - }, - E.Function => { - return Expr{ - .loc = loc, - .data = Data{ - .e_function = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.ImportMeta => { - return Expr{ - .loc = loc, - .data = Data{ - .e_import_meta = st, - }, - }; - }, - E.Call => { - return Expr{ - .loc = loc, - .data = Data{ - .e_call = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.Dot => { - return Expr{ - .loc = loc, - .data = Data{ - .e_dot = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.Index => { - return Expr{ - .loc = loc, - .data = Data{ - .e_index = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.Arrow => { - return Expr{ - .loc = loc, - .data = Data{ - .e_arrow = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.Identifier => { - return Expr{ - .loc = loc, - .data = Data{ - .e_identifier = E.Identifier{ - .ref = st.ref, - .must_keep_due_to_with_stmt = st.must_keep_due_to_with_stmt, - .can_be_removed_if_unused = st.can_be_removed_if_unused, - .call_can_be_unwrapped_if_unused = st.call_can_be_unwrapped_if_unused, - }, - }, - }; - }, - E.ImportIdentifier => { - return Expr{ - .loc = loc, - .data = Data{ - .e_import_identifier = .{ - .ref = st.ref, - .was_originally_identifier = st.was_originally_identifier, - }, - }, - }; - }, - E.CommonJSExportIdentifier => { - return Expr{ - .loc = loc, - .data = Data{ - .e_commonjs_export_identifier = .{ - .ref = st.ref, - }, - }, - }; - }, - - E.PrivateIdentifier => { - return Expr{ - .loc = loc, - .data = Data{ - .e_private_identifier = st, - }, - }; - }, - E.JSXElement => { - return Expr{ - .loc = loc, - .data = Data{ - .e_jsx_element = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.Missing => { - return Expr{ .loc = loc, .data = Data{ .e_missing = E.Missing{} } }; - }, - E.Number => { - return Expr{ - .loc = loc, - .data = Data{ - .e_number = st, - }, - }; - }, - E.BigInt => { - return Expr{ - .loc = loc, - .data = Data{ - .e_big_int = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.Object => { - return Expr{ - .loc = loc, - .data = Data{ - .e_object = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.Spread => { - return Expr{ - .loc = loc, - .data = Data{ - .e_spread = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.String => { - if (comptime Environment.isDebug) { - // Sanity check: assert string is not a null ptr - if (st.data.len > 0 and st.isUTF8()) { - bun.assert(@intFromPtr(st.data.ptr) > 0); - } - } - return Expr{ - .loc = loc, - .data = Data{ - .e_string = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - - E.Template => { - return Expr{ - .loc = loc, - .data = Data{ - .e_template = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.RegExp => { - return Expr{ - .loc = loc, - .data = Data{ - .e_reg_exp = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.Await => { - return Expr{ - .loc = loc, - .data = Data{ - .e_await = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.Yield => { - return Expr{ - .loc = loc, - .data = Data{ - .e_yield = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.If => { - return Expr{ - .loc = loc, - .data = Data{ - .e_if = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.RequireResolveString => { - return Expr{ - .loc = loc, - .data = Data{ - .e_require_resolve_string = st, - }, - }; - }, - E.Import => { - return Expr{ - .loc = loc, - .data = Data{ - .e_import = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st; - break :brk item; - }, - }, - }; - }, - E.RequireString => { - return Expr{ - .loc = loc, - .data = Data{ - .e_require_string = st, - }, - }; - }, - *E.String => { - return Expr{ - .loc = loc, - .data = Data{ - .e_string = brk: { - const item = allocator.create(Type) catch unreachable; - item.* = st.*; - break :brk item; - }, - }, - }; - }, - - else => { - @compileError("Invalid type passed to Expr.init: " ++ @typeName(Type)); - }, - } - } - - pub const Disabler = bun.DebugOnlyDisabler(@This()); - - pub fn init(comptime Type: type, st: Type, loc: logger.Loc) Expr { - icount += 1; - Data.Store.assert(); - - switch (Type) { - E.NameOfSymbol => { - return Expr{ - .loc = loc, - .data = Data{ - .e_name_of_symbol = Data.Store.append(E.NameOfSymbol, st), - }, - }; - }, - E.Array => { - return Expr{ - .loc = loc, - .data = Data{ - .e_array = Data.Store.append(Type, st), - }, - }; - }, - E.Class => { - return Expr{ - .loc = loc, - .data = Data{ - .e_class = Data.Store.append(Type, st), - }, - }; - }, - E.Unary => { - return Expr{ - .loc = loc, - .data = Data{ - .e_unary = Data.Store.append(Type, st), - }, - }; - }, - E.Binary => { - return Expr{ - .loc = loc, - .data = Data{ - .e_binary = Data.Store.append(Type, st), - }, - }; - }, - E.This => { - return Expr{ - .loc = loc, - .data = Data{ - .e_this = st, - }, - }; - }, - E.Boolean => { - return Expr{ - .loc = loc, - .data = Data{ - .e_boolean = st, - }, - }; - }, - E.Super => { - return Expr{ - .loc = loc, - .data = Data{ - .e_super = st, - }, - }; - }, - E.Null => { - return Expr{ - .loc = loc, - .data = Data{ - .e_null = st, - }, - }; - }, - E.Undefined => { - return Expr{ - .loc = loc, - .data = Data{ - .e_undefined = st, - }, - }; - }, - E.New => { - return Expr{ - .loc = loc, - .data = Data{ - .e_new = Data.Store.append(Type, st), - }, - }; - }, - E.NewTarget => { - return Expr{ - .loc = loc, - .data = Data{ - .e_new_target = st, - }, - }; - }, - E.Function => { - return Expr{ - .loc = loc, - .data = Data{ - .e_function = Data.Store.append(Type, st), - }, - }; - }, - E.ImportMeta => { - return Expr{ - .loc = loc, - .data = Data{ - .e_import_meta = st, - }, - }; - }, - E.Call => { - return Expr{ - .loc = loc, - .data = Data{ - .e_call = Data.Store.append(Type, st), - }, - }; - }, - E.Dot => { - return Expr{ - .loc = loc, - .data = Data{ - .e_dot = Data.Store.append(Type, st), - }, - }; - }, - E.Index => { - return Expr{ - .loc = loc, - .data = Data{ - .e_index = Data.Store.append(Type, st), - }, - }; - }, - E.Arrow => { - return Expr{ - .loc = loc, - .data = Data{ - .e_arrow = Data.Store.append(Type, st), - }, - }; - }, - E.Identifier => { - return Expr{ - .loc = loc, - .data = Data{ - .e_identifier = E.Identifier{ - .ref = st.ref, - .must_keep_due_to_with_stmt = st.must_keep_due_to_with_stmt, - .can_be_removed_if_unused = st.can_be_removed_if_unused, - .call_can_be_unwrapped_if_unused = st.call_can_be_unwrapped_if_unused, - }, - }, - }; - }, - E.ImportIdentifier => { - return Expr{ - .loc = loc, - .data = Data{ - .e_import_identifier = .{ - .ref = st.ref, - .was_originally_identifier = st.was_originally_identifier, - }, - }, - }; - }, - E.CommonJSExportIdentifier => { - return Expr{ - .loc = loc, - .data = Data{ - .e_commonjs_export_identifier = .{ - .ref = st.ref, - .base = st.base, - }, - }, - }; - }, - E.PrivateIdentifier => { - return Expr{ - .loc = loc, - .data = Data{ - .e_private_identifier = st, - }, - }; - }, - E.JSXElement => { - return Expr{ - .loc = loc, - .data = Data{ - .e_jsx_element = Data.Store.append(Type, st), - }, - }; - }, - E.Missing => { - return Expr{ .loc = loc, .data = Data{ .e_missing = E.Missing{} } }; - }, - E.Number => { - return Expr{ - .loc = loc, - .data = Data{ - .e_number = st, - }, - }; - }, - E.BigInt => { - return Expr{ - .loc = loc, - .data = Data{ - .e_big_int = Data.Store.append(Type, st), - }, - }; - }, - E.Object => { - return Expr{ - .loc = loc, - .data = Data{ - .e_object = Data.Store.append(Type, st), - }, - }; - }, - E.Spread => { - return Expr{ - .loc = loc, - .data = Data{ - .e_spread = Data.Store.append(Type, st), - }, - }; - }, - E.String => { - if (comptime Environment.isDebug) { - // Sanity check: assert string is not a null ptr - if (st.data.len > 0 and st.isUTF8()) { - bun.assert(@intFromPtr(st.data.ptr) > 0); - } - } - return Expr{ - .loc = loc, - .data = Data{ - .e_string = Data.Store.append(Type, st), - }, - }; - }, - - E.Template => { - return Expr{ - .loc = loc, - .data = Data{ - .e_template = Data.Store.append(Type, st), - }, - }; - }, - E.RegExp => { - return Expr{ - .loc = loc, - .data = Data{ - .e_reg_exp = Data.Store.append(Type, st), - }, - }; - }, - E.Await => { - return Expr{ - .loc = loc, - .data = Data{ - .e_await = Data.Store.append(Type, st), - }, - }; - }, - E.Yield => { - return Expr{ - .loc = loc, - .data = Data{ - .e_yield = Data.Store.append(Type, st), - }, - }; - }, - E.If => { - return Expr{ - .loc = loc, - .data = Data{ - .e_if = Data.Store.append(Type, st), - }, - }; - }, - E.RequireResolveString => { - return Expr{ - .loc = loc, - .data = Data{ - .e_require_resolve_string = st, - }, - }; - }, - E.Import => { - return Expr{ - .loc = loc, - .data = Data{ - .e_import = Data.Store.append(Type, st), - }, - }; - }, - E.RequireString => { - return Expr{ - .loc = loc, - .data = Data{ - .e_require_string = st, - }, - }; - }, - *E.String => { - return Expr{ - .loc = loc, - .data = Data{ - .e_string = Data.Store.append(@TypeOf(st.*), st.*), - }, - }; - }, - E.InlinedEnum => return .{ .loc = loc, .data = .{ - .e_inlined_enum = Data.Store.append(@TypeOf(st), st), - } }, - - else => { - @compileError("Invalid type passed to Expr.init: " ++ @typeName(Type)); - }, - } - } - - pub fn isPrimitiveLiteral(this: Expr) bool { - return @as(Tag, this.data).isPrimitiveLiteral(); - } - - pub fn isRef(this: Expr, ref: Ref) bool { - return switch (this.data) { - .e_import_identifier => |import_identifier| import_identifier.ref.eql(ref), - .e_identifier => |ident| ident.ref.eql(ref), - else => false, - }; - } - - pub const Tag = enum { - e_array, - e_unary, - e_binary, - e_class, - e_new, - e_function, - e_call, - e_dot, - e_index, - e_arrow, - e_jsx_element, - e_object, - e_spread, - e_template, - e_reg_exp, - e_await, - e_yield, - e_if, - e_import, - e_identifier, - e_import_identifier, - e_private_identifier, - e_commonjs_export_identifier, - e_boolean, - e_number, - e_big_int, - e_string, - e_require_string, - e_require_resolve_string, - e_require_call_target, - e_require_resolve_call_target, - e_missing, - e_this, - e_super, - e_null, - e_undefined, - e_new_target, - e_import_meta, - e_import_meta_main, - e_require_main, - e_special, - e_inlined_enum, - e_name_of_symbol, - - // object, regex and array may have had side effects - pub fn isPrimitiveLiteral(tag: Tag) bool { - return switch (tag) { - .e_null, .e_undefined, .e_string, .e_boolean, .e_number, .e_big_int => true, - else => false, - }; - } - - pub fn typeof(tag: Tag) ?string { - return switch (tag) { - .e_array, .e_object, .e_null, .e_reg_exp => "object", - .e_undefined => "undefined", - .e_boolean => "boolean", - .e_number => "number", - .e_big_int => "bigint", - .e_string => "string", - .e_class, .e_function, .e_arrow => "function", - else => null, - }; - } - - pub fn format(tag: Tag, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - try switch (tag) { - .e_string => writer.writeAll("string"), - .e_array => writer.writeAll("array"), - .e_unary => writer.writeAll("unary"), - .e_binary => writer.writeAll("binary"), - .e_boolean => writer.writeAll("boolean"), - .e_super => writer.writeAll("super"), - .e_null => writer.writeAll("null"), - .e_undefined => writer.writeAll("undefined"), - .e_new => writer.writeAll("new"), - .e_function => writer.writeAll("function"), - .e_new_target => writer.writeAll("new target"), - .e_import_meta => writer.writeAll("import.meta"), - .e_call => writer.writeAll("call"), - .e_dot => writer.writeAll("dot"), - .e_index => writer.writeAll("index"), - .e_arrow => writer.writeAll("arrow"), - .e_identifier => writer.writeAll("identifier"), - .e_import_identifier => writer.writeAll("import identifier"), - .e_private_identifier => writer.writeAll("#privateIdentifier"), - .e_jsx_element => writer.writeAll(""), - .e_missing => writer.writeAll(""), - .e_number => writer.writeAll("number"), - .e_big_int => writer.writeAll("BigInt"), - .e_object => writer.writeAll("object"), - .e_spread => writer.writeAll("..."), - .e_template => writer.writeAll("template"), - .e_reg_exp => writer.writeAll("regexp"), - .e_await => writer.writeAll("await"), - .e_yield => writer.writeAll("yield"), - .e_if => writer.writeAll("if"), - .e_require_resolve_string => writer.writeAll("require_or_require_resolve"), - .e_import => writer.writeAll("import"), - .e_this => writer.writeAll("this"), - .e_class => writer.writeAll("class"), - .e_require_string => writer.writeAll("require"), - else => writer.writeAll(@tagName(tag)), - }; - } - - pub fn jsonStringify(self: @This(), writer: anytype) !void { - return try writer.write(@tagName(self)); - } - - pub fn isArray(self: Tag) bool { - switch (self) { - .e_array => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isUnary(self: Tag) bool { - switch (self) { - .e_unary => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isBinary(self: Tag) bool { - switch (self) { - .e_binary => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isThis(self: Tag) bool { - switch (self) { - .e_this => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isClass(self: Tag) bool { - switch (self) { - .e_class => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isBoolean(self: Tag) bool { - switch (self) { - .e_boolean => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isSuper(self: Tag) bool { - switch (self) { - .e_super => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isNull(self: Tag) bool { - switch (self) { - .e_null => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isUndefined(self: Tag) bool { - switch (self) { - .e_undefined => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isNew(self: Tag) bool { - switch (self) { - .e_new => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isNewTarget(self: Tag) bool { - switch (self) { - .e_new_target => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isFunction(self: Tag) bool { - switch (self) { - .e_function => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isImportMeta(self: Tag) bool { - switch (self) { - .e_import_meta => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isCall(self: Tag) bool { - switch (self) { - .e_call => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isDot(self: Tag) bool { - switch (self) { - .e_dot => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isIndex(self: Tag) bool { - switch (self) { - .e_index => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isArrow(self: Tag) bool { - switch (self) { - .e_arrow => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isIdentifier(self: Tag) bool { - switch (self) { - .e_identifier => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isImportIdentifier(self: Tag) bool { - switch (self) { - .e_import_identifier => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isPrivateIdentifier(self: Tag) bool { - switch (self) { - .e_private_identifier => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isJsxElement(self: Tag) bool { - switch (self) { - .e_jsx_element => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isMissing(self: Tag) bool { - switch (self) { - .e_missing => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isNumber(self: Tag) bool { - switch (self) { - .e_number => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isBigInt(self: Tag) bool { - switch (self) { - .e_big_int => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isObject(self: Tag) bool { - switch (self) { - .e_object => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isSpread(self: Tag) bool { - switch (self) { - .e_spread => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isString(self: Tag) bool { - switch (self) { - .e_string => { - return true; - }, - else => { - return false; - }, - } - } - - pub fn isTemplate(self: Tag) bool { - switch (self) { - .e_template => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isRegExp(self: Tag) bool { - switch (self) { - .e_reg_exp => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isAwait(self: Tag) bool { - switch (self) { - .e_await => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isYield(self: Tag) bool { - switch (self) { - .e_yield => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isIf(self: Tag) bool { - switch (self) { - .e_if => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isRequireResolveString(self: Tag) bool { - switch (self) { - .e_require_resolve_string => { - return true; - }, - else => { - return false; - }, - } - } - pub fn isImport(self: Tag) bool { - switch (self) { - .e_import => { - return true; - }, - else => { - return false; - }, - } - } - }; - - pub fn isBoolean(a: Expr) bool { - switch (a.data) { - .e_boolean => { - return true; - }, - - .e_if => |ex| { - return isBoolean(ex.yes) and isBoolean(ex.no); - }, - .e_unary => |ex| { - return ex.op == .un_not or ex.op == .un_delete; - }, - .e_binary => |ex| { - switch (ex.op) { - .bin_strict_eq, .bin_strict_ne, .bin_loose_eq, .bin_loose_ne, .bin_lt, .bin_gt, .bin_le, .bin_ge, .bin_instanceof, .bin_in => { - return true; - }, - .bin_logical_or => { - return isBoolean(ex.left) and isBoolean(ex.right); - }, - .bin_logical_and => { - return isBoolean(ex.left) and isBoolean(ex.right); - }, - else => {}, - } - }, - else => {}, - } - - return false; - } - - pub fn assign(a: Expr, b: Expr) Expr { - return init(E.Binary, E.Binary{ - .op = .bin_assign, - .left = a, - .right = b, - }, a.loc); - } - pub inline fn at(expr: Expr, comptime Type: type, t: Type, _: std.mem.Allocator) Expr { - return init(Type, t, expr.loc); - } - - // Wraps the provided expression in the "!" prefix operator. The expression - // will potentially be simplified to avoid generating unnecessary extra "!" - // operators. For example, calling this with "!!x" will return "!x" instead - // of returning "!!!x". - pub fn not(expr: Expr, allocator: std.mem.Allocator) Expr { - return maybeSimplifyNot( - expr, - allocator, - ) orelse Expr.init( - E.Unary, - E.Unary{ - .op = .un_not, - .value = expr, - }, - expr.loc, - ); - } - - pub fn hasValueForThisInCall(expr: Expr) bool { - return switch (expr.data) { - .e_dot, .e_index => true, - else => false, - }; - } - - /// 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 => { - return expr.at(E.Boolean, E.Boolean{ .value = true }, allocator); - }, - .e_boolean => |b| { - return expr.at(E.Boolean, E.Boolean{ .value = b.value }, allocator); - }, - .e_number => |n| { - return expr.at(E.Boolean, E.Boolean{ .value = (n.value == 0 or std.math.isNan(n.value)) }, allocator); - }, - .e_big_int => |b| { - return expr.at(E.Boolean, E.Boolean{ .value = strings.eqlComptime(b.value, "0") }, allocator); - }, - .e_function, - .e_arrow, - .e_reg_exp, - => { - return expr.at(E.Boolean, E.Boolean{ .value = false }, allocator); - }, - // "!!!a" => "!a" - .e_unary => |un| { - if (un.op == Op.Code.un_not and knownPrimitive(un.value) == .boolean) { - return un.value; - } - }, - .e_binary => |ex| { - // TODO: evaluate whether or not it is safe to do this mutation since it's modifying in-place. - // Make sure that these transformations are all safe for special values. - // For example, "!(a < b)" is not the same as "a >= b" if a and/or b are - // NaN (or undefined, or null, or possibly other problem cases too). - switch (ex.op) { - Op.Code.bin_loose_eq => { - // "!(a == b)" => "a != b" - ex.op = .bin_loose_ne; - return expr; - }, - Op.Code.bin_loose_ne => { - // "!(a != b)" => "a == b" - ex.op = .bin_loose_eq; - return expr; - }, - Op.Code.bin_strict_eq => { - // "!(a === b)" => "a !== b" - ex.op = .bin_strict_ne; - return expr; - }, - Op.Code.bin_strict_ne => { - // "!(a !== b)" => "a === b" - ex.op = .bin_strict_eq; - return expr; - }, - Op.Code.bin_comma => { - // "!(a, b)" => "a, !b" - ex.right = ex.right.not(allocator); - return expr; - }, - else => {}, - } - }, - .e_inlined_enum => |inlined| { - return maybeSimplifyNot(inlined.value, allocator); - }, - - else => {}, - } - - return null; - } - - pub fn toStringExprWithoutSideEffects(expr: Expr, allocator: std.mem.Allocator) ?Expr { - const unwrapped = expr.unwrapInlined(); - const slice = switch (unwrapped.data) { - .e_null => "null", - .e_string => return expr, - .e_undefined => "undefined", - .e_boolean => |data| if (data.value) "true" else "false", - .e_big_int => |bigint| bigint.value, - .e_number => |num| if (num.toString(allocator)) |str| - str - else - null, - .e_reg_exp => |regexp| regexp.value, - .e_dot => |dot| @as(?[]const u8, brk: { - // This is dumb but some JavaScript obfuscators use this to generate string literals - if (bun.strings.eqlComptime(dot.name, "constructor")) { - break :brk switch (dot.target.data) { - .e_string => "function String() { [native code] }", - .e_reg_exp => "function RegExp() { [native code] }", - else => null, - }; - } - break :brk null; - }), - else => null, - }; - return if (slice) |s| Expr.init(E.String, E.String.init(s), expr.loc) else null; - } - - pub fn isOptionalChain(self: *const @This()) bool { - return switch (self.data) { - .e_dot => self.data.e_dot.optional_chain != null, - .e_index => self.data.e_index.optional_chain != null, - .e_call => self.data.e_call.optional_chain != null, - else => false, - }; - } - - pub inline fn knownPrimitive(self: @This()) PrimitiveType { - return self.data.knownPrimitive(); - } - - pub const PrimitiveType = enum { - unknown, - mixed, - null, - undefined, - boolean, - number, - string, - bigint, - - pub const static = std.enums.EnumSet(PrimitiveType).init(.{ - .mixed = true, - .null = true, - .undefined = true, - .boolean = true, - .number = true, - .string = true, - // for our purposes, bigint is dynamic - // it is technically static though - // .@"bigint" = true, - }); - - pub inline fn isStatic(this: PrimitiveType) bool { - return static.contains(this); - } - - pub fn merge(left_known: PrimitiveType, right_known: PrimitiveType) PrimitiveType { - if (right_known == .unknown or left_known == .unknown) - return .unknown; - - return if (left_known == right_known) - left_known - else - .mixed; - } - }; - - pub const Data = union(Tag) { - e_array: *E.Array, - e_unary: *E.Unary, - e_binary: *E.Binary, - e_class: *E.Class, - - e_new: *E.New, - e_function: *E.Function, - e_call: *E.Call, - e_dot: *E.Dot, - e_index: *E.Index, - e_arrow: *E.Arrow, - - e_jsx_element: *E.JSXElement, - e_object: *E.Object, - e_spread: *E.Spread, - e_template: *E.Template, - e_reg_exp: *E.RegExp, - e_await: *E.Await, - e_yield: *E.Yield, - e_if: *E.If, - e_import: *E.Import, - - e_identifier: E.Identifier, - e_import_identifier: E.ImportIdentifier, - e_private_identifier: E.PrivateIdentifier, - e_commonjs_export_identifier: E.CommonJSExportIdentifier, - - e_boolean: E.Boolean, - e_number: E.Number, - e_big_int: *E.BigInt, - e_string: *E.String, - - e_require_string: E.RequireString, - e_require_resolve_string: E.RequireResolveString, - e_require_call_target, - e_require_resolve_call_target, - - e_missing: E.Missing, - e_this: E.This, - e_super: E.Super, - e_null: E.Null, - e_undefined: E.Undefined, - e_new_target: E.NewTarget, - e_import_meta: E.ImportMeta, - - e_import_meta_main: E.ImportMetaMain, - e_require_main, - - /// Covers some exotic AST node types under one namespace, since the - /// places this is found it all follows similar handling. - e_special: E.Special, - - e_inlined_enum: *E.InlinedEnum, - - e_name_of_symbol: *E.NameOfSymbol, - - comptime { - bun.assert_eql(@sizeOf(Data), 24); // Do not increase the size of Expr - } - - pub fn as(data: Data, comptime tag: Tag) ?@FieldType(Data, @tagName(tag)) { - return if (data == tag) @field(data, @tagName(tag)) else null; - } - - pub fn clone(this: Expr.Data, allocator: std.mem.Allocator) !Data { - return switch (this) { - .e_array => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_array))); - item.* = el.*; - return .{ .e_array = item }; - }, - .e_unary => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_unary))); - item.* = el.*; - return .{ .e_unary = item }; - }, - .e_binary => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_binary))); - item.* = el.*; - return .{ .e_binary = item }; - }, - .e_class => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_class))); - item.* = el.*; - return .{ .e_class = item }; - }, - .e_new => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_new))); - item.* = el.*; - return .{ .e_new = item }; - }, - .e_function => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_function))); - item.* = el.*; - return .{ .e_function = item }; - }, - .e_call => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_call))); - item.* = el.*; - return .{ .e_call = item }; - }, - .e_dot => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_dot))); - item.* = el.*; - return .{ .e_dot = item }; - }, - .e_index => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_index))); - item.* = el.*; - return .{ .e_index = item }; - }, - .e_arrow => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_arrow))); - item.* = el.*; - return .{ .e_arrow = item }; - }, - .e_jsx_element => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_jsx_element))); - item.* = el.*; - return .{ .e_jsx_element = item }; - }, - .e_object => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_object))); - item.* = el.*; - return .{ .e_object = item }; - }, - .e_spread => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_spread))); - item.* = el.*; - return .{ .e_spread = item }; - }, - .e_template => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_template))); - item.* = el.*; - return .{ .e_template = item }; - }, - .e_reg_exp => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_reg_exp))); - item.* = el.*; - return .{ .e_reg_exp = item }; - }, - .e_await => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_await))); - item.* = el.*; - return .{ .e_await = item }; - }, - .e_yield => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_yield))); - item.* = el.*; - return .{ .e_yield = item }; - }, - .e_if => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_if))); - item.* = el.*; - return .{ .e_if = item }; - }, - .e_import => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_import))); - item.* = el.*; - return .{ .e_import = item }; - }, - .e_big_int => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_big_int))); - item.* = el.*; - return .{ .e_big_int = item }; - }, - .e_string => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_string))); - item.* = el.*; - return .{ .e_string = item }; - }, - .e_inlined_enum => |el| { - const item = try allocator.create(std.meta.Child(@TypeOf(this.e_inlined_enum))); - item.* = el.*; - return .{ .e_inlined_enum = item }; - }, - else => this, - }; - } - - pub fn deepClone(this: Expr.Data, allocator: std.mem.Allocator) !Data { - return switch (this) { - .e_array => |el| { - const items = try el.items.deepClone(allocator); - const item = bun.create(allocator, E.Array, .{ - .items = items, - .comma_after_spread = el.comma_after_spread, - .was_originally_macro = el.was_originally_macro, - .is_single_line = el.is_single_line, - .is_parenthesized = el.is_parenthesized, - .close_bracket_loc = el.close_bracket_loc, - }); - return .{ .e_array = item }; - }, - .e_unary => |el| { - const item = bun.create(allocator, E.Unary, .{ - .op = el.op, - .value = try el.value.deepClone(allocator), - }); - return .{ .e_unary = item }; - }, - .e_binary => |el| { - const item = bun.create(allocator, E.Binary, .{ - .op = el.op, - .left = try el.left.deepClone(allocator), - .right = try el.right.deepClone(allocator), - }); - return .{ .e_binary = item }; - }, - .e_class => |el| { - const properties = try allocator.alloc(G.Property, el.properties.len); - for (el.properties, 0..) |prop, i| { - properties[i] = try prop.deepClone(allocator); - } - - const item = bun.create(allocator, E.Class, .{ - .class_keyword = el.class_keyword, - .ts_decorators = try el.ts_decorators.deepClone(allocator), - .class_name = el.class_name, - .extends = if (el.extends) |e| try e.deepClone(allocator) else null, - .body_loc = el.body_loc, - .close_brace_loc = el.close_brace_loc, - .properties = properties, - .has_decorators = el.has_decorators, - }); - return .{ .e_class = item }; - }, - .e_new => |el| { - const item = bun.create(allocator, E.New, .{ - .target = try el.target.deepClone(allocator), - .args = try el.args.deepClone(allocator), - .can_be_unwrapped_if_unused = el.can_be_unwrapped_if_unused, - .close_parens_loc = el.close_parens_loc, - }); - - return .{ .e_new = item }; - }, - .e_function => |el| { - const item = bun.create(allocator, E.Function, .{ - .func = try el.func.deepClone(allocator), - }); - return .{ .e_function = item }; - }, - .e_call => |el| { - const item = bun.create(allocator, E.Call, .{ - .target = try el.target.deepClone(allocator), - .args = try el.args.deepClone(allocator), - .optional_chain = el.optional_chain, - .is_direct_eval = el.is_direct_eval, - .close_paren_loc = el.close_paren_loc, - .can_be_unwrapped_if_unused = el.can_be_unwrapped_if_unused, - .was_jsx_element = el.was_jsx_element, - }); - return .{ .e_call = item }; - }, - .e_dot => |el| { - const item = bun.create(allocator, E.Dot, .{ - .target = try el.target.deepClone(allocator), - .name = el.name, - .name_loc = el.name_loc, - .optional_chain = el.optional_chain, - .can_be_removed_if_unused = el.can_be_removed_if_unused, - .call_can_be_unwrapped_if_unused = el.call_can_be_unwrapped_if_unused, - }); - return .{ .e_dot = item }; - }, - .e_index => |el| { - const item = bun.create(allocator, E.Index, .{ - .target = try el.target.deepClone(allocator), - .index = try el.index.deepClone(allocator), - .optional_chain = el.optional_chain, - }); - return .{ .e_index = item }; - }, - .e_arrow => |el| { - const args = try allocator.alloc(G.Arg, el.args.len); - for (0..args.len) |i| { - args[i] = try el.args[i].deepClone(allocator); - } - const item = bun.create(allocator, E.Arrow, .{ - .args = args, - .body = el.body, - .is_async = el.is_async, - .has_rest_arg = el.has_rest_arg, - .prefer_expr = el.prefer_expr, - }); - - return .{ .e_arrow = item }; - }, - .e_jsx_element => |el| { - const item = bun.create(allocator, E.JSXElement, .{ - .tag = if (el.tag) |tag| try tag.deepClone(allocator) else null, - .properties = try el.properties.deepClone(allocator), - .children = try el.children.deepClone(allocator), - .key_prop_index = el.key_prop_index, - .flags = el.flags, - .close_tag_loc = el.close_tag_loc, - }); - return .{ .e_jsx_element = item }; - }, - .e_object => |el| { - const item = bun.create(allocator, E.Object, .{ - .properties = try el.properties.deepClone(allocator), - .comma_after_spread = el.comma_after_spread, - .is_single_line = el.is_single_line, - .is_parenthesized = el.is_parenthesized, - .was_originally_macro = el.was_originally_macro, - .close_brace_loc = el.close_brace_loc, - }); - return .{ .e_object = item }; - }, - .e_spread => |el| { - const item = bun.create(allocator, E.Spread, .{ - .value = try el.value.deepClone(allocator), - }); - return .{ .e_spread = item }; - }, - .e_template => |el| { - const item = bun.create(allocator, E.Template, .{ - .tag = if (el.tag) |tag| try tag.deepClone(allocator) else null, - .parts = el.parts, - .head = el.head, - }); - return .{ .e_template = item }; - }, - .e_reg_exp => |el| { - const item = bun.create(allocator, E.RegExp, .{ - .value = el.value, - .flags_offset = el.flags_offset, - }); - return .{ .e_reg_exp = item }; - }, - .e_await => |el| { - const item = bun.create(allocator, E.Await, .{ - .value = try el.value.deepClone(allocator), - }); - return .{ .e_await = item }; - }, - .e_yield => |el| { - const item = bun.create(allocator, E.Yield, .{ - .value = if (el.value) |value| try value.deepClone(allocator) else null, - .is_star = el.is_star, - }); - return .{ .e_yield = item }; - }, - .e_if => |el| { - const item = bun.create(allocator, E.If, .{ - .test_ = try el.test_.deepClone(allocator), - .yes = try el.yes.deepClone(allocator), - .no = try el.no.deepClone(allocator), - }); - return .{ .e_if = item }; - }, - .e_import => |el| { - const item = bun.create(allocator, E.Import, .{ - .expr = try el.expr.deepClone(allocator), - .options = try el.options.deepClone(allocator), - .import_record_index = el.import_record_index, - }); - return .{ .e_import = item }; - }, - .e_big_int => |el| { - const item = bun.create(allocator, E.BigInt, .{ - .value = el.value, - }); - return .{ .e_big_int = item }; - }, - .e_string => |el| { - const item = bun.create(allocator, E.String, .{ - .data = el.data, - .prefer_template = el.prefer_template, - .next = el.next, - .end = el.end, - .rope_len = el.rope_len, - .is_utf16 = el.is_utf16, - }); - return .{ .e_string = item }; - }, - .e_inlined_enum => |el| { - const item = bun.create(allocator, E.InlinedEnum, .{ - .value = el.value, - .comment = el.comment, - }); - return .{ .e_inlined_enum = item }; - }, - else => this, - }; - } - - /// `hasher` should be something with 'pub fn update([]const u8) void'; - /// symbol table is passed to serialize `Ref` as an identifier names instead of a nondeterministic numbers - pub fn writeToHasher(this: Expr.Data, hasher: anytype, symbol_table: anytype) void { - writeAnyToHasher(hasher, std.meta.activeTag(this)); - switch (this) { - .e_name_of_symbol => |e| { - const symbol = e.ref.getSymbol(symbol_table); - hasher.update(symbol.original_name); - }, - .e_array => |e| { - writeAnyToHasher(hasher, .{ - e.is_single_line, - e.is_parenthesized, - e.was_originally_macro, - e.items.len, - }); - for (e.items.slice()) |item| { - item.data.writeToHasher(hasher, symbol_table); - } - }, - .e_unary => |e| { - writeAnyToHasher(hasher, .{e.op}); - e.value.data.writeToHasher(hasher, symbol_table); - }, - .e_binary => |e| { - writeAnyToHasher(hasher, .{e.op}); - e.left.data.writeToHasher(hasher, symbol_table); - e.right.data.writeToHasher(hasher, symbol_table); - }, - .e_class => |e| { - _ = e; // autofix - }, - inline .e_new, .e_call => |e| { - _ = e; // autofix - }, - .e_function => |e| { - _ = e; // autofix - }, - .e_dot => |e| { - writeAnyToHasher(hasher, .{ e.optional_chain, e.name.len }); - e.target.data.writeToHasher(hasher, symbol_table); - hasher.update(e.name); - }, - .e_index => |e| { - writeAnyToHasher(hasher, .{e.optional_chain}); - e.target.data.writeToHasher(hasher, symbol_table); - e.index.data.writeToHasher(hasher, symbol_table); - }, - .e_arrow => |e| { - _ = e; // autofix - }, - .e_jsx_element => |e| { - _ = e; // autofix - }, - .e_object => |e| { - _ = e; // autofix - }, - inline .e_spread, .e_await => |e| { - e.value.data.writeToHasher(hasher, symbol_table); - }, - inline .e_yield => |e| { - writeAnyToHasher(hasher, .{ e.is_star, e.value }); - if (e.value) |value| - value.data.writeToHasher(hasher, symbol_table); - }, - .e_template => |e| { - _ = e; // autofix - }, - .e_if => |e| { - _ = e; // autofix - }, - .e_import => |e| { - _ = e; // autofix - - }, - inline .e_identifier, - .e_import_identifier, - .e_private_identifier, - .e_commonjs_export_identifier, - => |e| { - const symbol = e.ref.getSymbol(symbol_table); - hasher.update(symbol.original_name); - }, - inline .e_boolean, .e_number => |e| { - writeAnyToHasher(hasher, e.value); - }, - inline .e_big_int, .e_reg_exp => |e| { - hasher.update(e.value); - }, - - .e_string => |e| { - var next: ?*E.String = e; - if (next) |current| { - if (current.isUTF8()) { - hasher.update(current.data); - } else { - hasher.update(bun.reinterpretSlice(u8, current.slice16())); - } - next = current.next; - hasher.update("\x00"); - } - }, - inline .e_require_string, .e_require_resolve_string => |e| { - writeAnyToHasher(hasher, e.import_record_index); // preferably, i'd like to write the filepath - }, - - .e_import_meta_main => |e| { - writeAnyToHasher(hasher, e.inverted); - }, - .e_inlined_enum => |e| { - // pretend there is no comment - e.value.data.writeToHasher(hasher, symbol_table); - }, - - // no data - .e_require_call_target, - .e_require_resolve_call_target, - .e_missing, - .e_this, - .e_super, - .e_null, - .e_undefined, - .e_new_target, - .e_require_main, - .e_import_meta, - .e_special, - => {}, - } - } - - /// "const values" here refers to expressions that can participate in constant - /// inlining, as they have no side effects on instantiation, and there would be - /// no observable difference if duplicated. This is a subset of canBeMoved() - pub fn canBeConstValue(this: Expr.Data) bool { - return switch (this) { - .e_number, - .e_boolean, - .e_null, - .e_undefined, - .e_inlined_enum, - => true, - .e_string => |str| str.next == null, - .e_array => |array| array.was_originally_macro, - .e_object => |object| object.was_originally_macro, - else => false, - }; - } - - /// Expressions that can be moved are those that do not have side - /// effects on their own. This is used to determine what can be moved - /// outside of a module wrapper (__esm/__commonJS). - pub fn canBeMoved(data: Expr.Data) bool { - return switch (data) { - // TODO: identifiers can be removed if unused, however code that - // moves expressions around sometimes does so incorrectly when - // doing destructures. test case: https://github.com/oven-sh/bun/issues/14027 - // .e_identifier => |id| id.can_be_removed_if_unused, - - .e_class => |class| class.canBeMoved(), - - .e_arrow, - .e_function, - - .e_number, - .e_boolean, - .e_null, - .e_undefined, - // .e_reg_exp, - .e_big_int, - .e_string, - .e_inlined_enum, - .e_import_meta, - => true, - - .e_template => |template| template.tag == null and template.parts.len == 0, - - .e_array => |array| array.was_originally_macro, - .e_object => |object| object.was_originally_macro, - - // TODO: experiment with allowing some e_binary, e_unary, e_if as movable - - else => false, - }; - } - - pub fn knownPrimitive(data: Expr.Data) PrimitiveType { - return switch (data) { - .e_big_int => .bigint, - .e_boolean => .boolean, - .e_null => .null, - .e_number => .number, - .e_string => .string, - .e_undefined => .undefined, - .e_template => if (data.e_template.tag == null) PrimitiveType.string else PrimitiveType.unknown, - .e_if => mergeKnownPrimitive(data.e_if.yes.data, data.e_if.no.data), - .e_binary => |binary| brk: { - switch (binary.op) { - .bin_strict_eq, - .bin_strict_ne, - .bin_loose_eq, - .bin_loose_ne, - .bin_lt, - .bin_gt, - .bin_le, - .bin_ge, - .bin_instanceof, - .bin_in, - => break :brk PrimitiveType.boolean, - .bin_logical_or, .bin_logical_and => break :brk binary.left.data.mergeKnownPrimitive(binary.right.data), - - .bin_nullish_coalescing => { - const left = binary.left.data.knownPrimitive(); - const right = binary.right.data.knownPrimitive(); - if (left == .null or left == .undefined) - break :brk right; - - if (left != .unknown) { - if (left != .mixed) - break :brk left; // Definitely not null or undefined - - if (right != .unknown) - break :brk PrimitiveType.mixed; // Definitely some kind of primitive - } - }, - - .bin_add => { - const left = binary.left.data.knownPrimitive(); - const right = binary.right.data.knownPrimitive(); - - if (left == .string or right == .string) - break :brk PrimitiveType.string; - - if (left == .bigint or right == .bigint) - break :brk PrimitiveType.bigint; - - if (switch (left) { - .unknown, .mixed, .bigint => false, - else => true, - } and switch (right) { - .unknown, .mixed, .bigint => false, - else => true, - }) - break :brk PrimitiveType.number; - - break :brk PrimitiveType.mixed; // Can be number or bigint or string (or an exception) - }, - - .bin_sub, - .bin_sub_assign, - .bin_mul, - .bin_mul_assign, - .bin_div, - .bin_div_assign, - .bin_rem, - .bin_rem_assign, - .bin_pow, - .bin_pow_assign, - .bin_bitwise_and, - .bin_bitwise_and_assign, - .bin_bitwise_or, - .bin_bitwise_or_assign, - .bin_bitwise_xor, - .bin_bitwise_xor_assign, - .bin_shl, - .bin_shl_assign, - .bin_shr, - .bin_shr_assign, - .bin_u_shr, - .bin_u_shr_assign, - => break :brk PrimitiveType.mixed, // Can be number or bigint (or an exception) - - .bin_assign, - .bin_comma, - => break :brk binary.right.data.knownPrimitive(), - - else => {}, - } - - break :brk PrimitiveType.unknown; - }, - - .e_unary => switch (data.e_unary.op) { - .un_void => PrimitiveType.undefined, - .un_typeof => PrimitiveType.string, - .un_not, .un_delete => PrimitiveType.boolean, - .un_pos => PrimitiveType.number, // Cannot be bigint because that throws an exception - .un_neg, .un_cpl => switch (data.e_unary.value.data.knownPrimitive()) { - .bigint => PrimitiveType.bigint, - .unknown, .mixed => PrimitiveType.mixed, - else => PrimitiveType.number, // Can be number or bigint - }, - .un_pre_dec, .un_pre_inc, .un_post_dec, .un_post_inc => PrimitiveType.mixed, // Can be number or bigint - - else => PrimitiveType.unknown, - }, - - .e_inlined_enum => |inlined| inlined.value.data.knownPrimitive(), - - else => PrimitiveType.unknown, - }; - } - - pub fn mergeKnownPrimitive(lhs: Expr.Data, rhs: Expr.Data) PrimitiveType { - return lhs.knownPrimitive().merge(rhs.knownPrimitive()); - } - - /// Returns true if the result of the "typeof" operator on this expression is - /// statically determined and this expression has no side effects (i.e. can be - /// removed without consequence). - pub inline fn toTypeof(data: Expr.Data) ?string { - return @as(Expr.Tag, data).typeof(); - } - - pub fn toNumber(data: Expr.Data) ?f64 { - return switch (data) { - .e_null => 0, - .e_undefined => std.math.nan(f64), - .e_string => |str| { - if (str.next != null) return null; - if (!str.isUTF8()) return null; - - // +'1' => 1 - return stringToEquivalentNumberValue(str.slice8()); - }, - .e_boolean => @as(f64, if (data.e_boolean.value) 1.0 else 0.0), - .e_number => data.e_number.value, - .e_inlined_enum => |inlined| switch (inlined.value.data) { - .e_number => |num| num.value, - .e_string => |str| { - if (str.next != null) return null; - if (!str.isUTF8()) return null; - - // +'1' => 1 - return stringToEquivalentNumberValue(str.slice8()); - }, - else => null, - }, - else => null, - }; - } - - pub fn toFiniteNumber(data: Expr.Data) ?f64 { - return switch (data) { - .e_boolean => @as(f64, if (data.e_boolean.value) 1.0 else 0.0), - .e_number => if (std.math.isFinite(data.e_number.value)) - data.e_number.value - else - null, - .e_inlined_enum => |inlined| switch (inlined.value.data) { - .e_number => |num| if (std.math.isFinite(num.value)) - num.value - else - null, - else => null, - }, - else => null, - }; - } - - pub fn extractNumericValue(data: Expr.Data) ?f64 { - return switch (data) { - .e_number => data.e_number.value, - .e_inlined_enum => |inlined| switch (inlined.value.data) { - .e_number => |num| num.value, - else => null, - }, - else => null, - }; - } - - pub const Equality = struct { - equal: bool = false, - ok: bool = false, - - /// This extra flag is unfortunately required for the case of visiting the expression - /// `require.main === module` (and any combination of !==, ==, !=, either ordering) - /// - /// We want to replace this with the dedicated import_meta_main node, which: - /// - Stops this module from having p.require_ref, allowing conversion to ESM - /// - Allows us to inline `import.meta.main`'s value, if it is known (bun build --compile) - is_require_main_and_module: bool = false, - - pub const @"true" = Equality{ .ok = true, .equal = true }; - pub const @"false" = Equality{ .ok = true, .equal = false }; - pub const unknown = Equality{ .ok = false }; - }; - - // Returns "equal, ok". If "ok" is false, then nothing is known about the two - // values. If "ok" is true, the equality or inequality of the two values is - // stored in "equal". - pub fn eql( - left: Expr.Data, - right: Expr.Data, - p: anytype, - comptime kind: enum { loose, strict }, - ) Equality { - comptime bun.assert(@typeInfo(@TypeOf(p)).pointer.size == .one); // pass *Parser - - // https://dorey.github.io/JavaScript-Equality-Table/ - switch (left) { - .e_inlined_enum => |inlined| return inlined.value.data.eql(right, p, kind), - - .e_null, .e_undefined => { - const ok = switch (@as(Expr.Tag, right)) { - .e_null, .e_undefined => true, - else => @as(Expr.Tag, right).isPrimitiveLiteral(), - }; - - if (comptime kind == .loose) { - return .{ - .equal = switch (@as(Expr.Tag, right)) { - .e_null, .e_undefined => true, - else => false, - }, - .ok = ok, - }; - } - - return .{ - .equal = @as(Tag, right) == @as(Tag, left), - .ok = ok, - }; - }, - .e_boolean => |l| { - switch (right) { - .e_boolean => { - return .{ - .ok = true, - .equal = l.value == right.e_boolean.value, - }; - }, - .e_number => |num| { - if (comptime kind == .strict) { - // "true === 1" is false - // "false === 0" is false - return Equality.false; - } - - return .{ - .ok = true, - .equal = if (l.value) - num.value == 1 - else - num.value == 0, - }; - }, - .e_null, .e_undefined => { - return Equality.false; - }, - else => {}, - } - }, - .e_number => |l| { - switch (right) { - .e_number => |r| { - return .{ - .ok = true, - .equal = l.value == r.value, - }; - }, - .e_inlined_enum => |r| if (r.value.data == .e_number) { - return .{ - .ok = true, - .equal = l.value == r.value.data.e_number.value, - }; - }, - .e_boolean => |r| { - if (comptime kind == .loose) { - return .{ - .ok = true, - // "1 == true" is true - // "0 == false" is true - .equal = if (r.value) - l.value == 1 - else - l.value == 0, - }; - } - - // "1 === true" is false - // "0 === false" is false - return Equality.false; - }, - .e_null, .e_undefined => { - // "(not null or undefined) == undefined" is false - return Equality.false; - }, - else => {}, - } - }, - .e_big_int => |l| { - if (right == .e_big_int) { - if (strings.eqlLong(l.value, right.e_big_int.value, true)) { - return Equality.true; - } - - // 0x0000n == 0n is true - return .{ .ok = false }; - } else { - return .{ - .ok = switch (right) { - .e_null, .e_undefined => true, - else => false, - }, - .equal = false, - }; - } - }, - .e_string => |l| { - switch (right) { - .e_string => |r| { - r.resolveRopeIfNeeded(p.allocator); - l.resolveRopeIfNeeded(p.allocator); - return .{ - .ok = true, - .equal = r.eql(E.String, l), - }; - }, - .e_inlined_enum => |inlined| { - if (inlined.value.data == .e_string) { - const r = inlined.value.data.e_string; - - r.resolveRopeIfNeeded(p.allocator); - l.resolveRopeIfNeeded(p.allocator); - - return .{ - .ok = true, - .equal = r.eql(E.String, l), - }; - } - }, - .e_null, .e_undefined => { - return Equality.false; - }, - .e_number => |r| { - if (comptime kind == .loose) { - l.resolveRopeIfNeeded(p.allocator); - if (r.value == 0 and (l.isBlank() or l.eqlComptime("0"))) { - return Equality.true; - } - - if (r.value == 1 and l.eqlComptime("1")) { - return Equality.true; - } - - // the string could still equal 0 or 1 but it could be hex, binary, octal, ... - return Equality.unknown; - } else { - return Equality.false; - } - }, - - else => {}, - } - }, - - else => { - // Do not need to check left because e_require_main is - // always re-ordered to the right side. - if (right == .e_require_main) { - if (left.as(.e_identifier)) |id| { - if (id.ref.eql(p.module_ref)) return .{ - .ok = true, - .equal = true, - .is_require_main_and_module = true, - }; - } - } - }, - } - - return Equality.unknown; - } - - pub fn toJS(this: Data, allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) ToJSError!JSC.JSValue { - return switch (this) { - .e_array => |e| e.toJS(allocator, globalObject), - .e_object => |e| e.toJS(allocator, globalObject), - .e_string => |e| e.toJS(allocator, globalObject), - .e_null => JSC.JSValue.null, - .e_undefined => .js_undefined, - .e_boolean => |boolean| if (boolean.value) - JSC.JSValue.true - else - JSC.JSValue.false, - .e_number => |e| e.toJS(), - // .e_big_int => |e| e.toJS(ctx, exception), - - .e_inlined_enum => |inlined| inlined.value.data.toJS(allocator, globalObject), - - .e_identifier, - .e_import_identifier, - .e_private_identifier, - .e_commonjs_export_identifier, - => error.@"Cannot convert identifier to JS. Try a statically-known value", - - // brk: { - // // var node = try allocator.create(Macro.JSNode); - // // node.* = Macro.JSNode.initExpr(Expr{ .data = this, .loc = logger.Loc.Empty }); - // // break :brk JSC.JSValue.c(Macro.JSNode.Class.make(globalObject, node)); - // }, - - else => { - return error.@"Cannot convert argument type to JS"; - }, - }; - } - - pub const Store = struct { - const StoreType = NewStore(&.{ - E.NameOfSymbol, - E.Array, - E.Arrow, - E.Await, - E.BigInt, - E.Binary, - E.Call, - E.Class, - E.Dot, - E.Function, - E.If, - E.Import, - E.Index, - E.InlinedEnum, - E.JSXElement, - E.New, - E.Number, - E.Object, - E.PrivateIdentifier, - E.RegExp, - E.Spread, - E.String, - E.Template, - E.TemplatePart, - E.Unary, - E.Yield, - }, 512); - - pub threadlocal var instance: ?*StoreType = null; - pub threadlocal var memory_allocator: ?*ASTMemoryAllocator = null; - pub threadlocal var disable_reset = false; - - pub fn create() void { - if (instance != null or memory_allocator != null) { - return; - } - - instance = StoreType.init(); - } - - pub fn reset() void { - if (disable_reset or memory_allocator != null) return; - instance.?.reset(); - } - - pub fn deinit() void { - if (instance == null or memory_allocator != null) return; - instance.?.deinit(); - instance = null; - } - - pub inline fn assert() void { - if (comptime Environment.isDebug or Environment.enable_asan) { - if (instance == null and memory_allocator == null) - bun.unreachablePanic("Store must be init'd", .{}); - } - } - - /// create || reset - pub fn begin() void { - if (memory_allocator != null) return; - if (instance == null) { - create(); - return; - } - - if (!disable_reset) - instance.?.reset(); - } - - pub fn append(comptime T: type, value: T) *T { - if (memory_allocator) |allocator| { - return allocator.append(T, value); - } - - Disabler.assert(); - return instance.?.append(T, value); - } - }; - - pub inline fn isStringValue(self: Data) bool { - return @as(Expr.Tag, self) == .e_string; - } - }; - - pub fn StoredData(tag: Tag) type { - const T = @FieldType(Data, tag); - return switch (@typeInfo(T)) { - .pointer => |ptr| ptr.child, - else => T, - }; - } -}; - pub const EnumValue = struct { loc: logger.Loc, ref: Ref, @@ -6478,222 +214,6 @@ pub const EnumValue = struct { } }; -pub const S = struct { - pub const Block = struct { - stmts: StmtNodeList, - close_brace_loc: logger.Loc = logger.Loc.Empty, - }; - - pub const SExpr = struct { - value: ExprNodeIndex, - - // This is set to true for automatically-generated expressions that should - // not affect tree shaking. For example, calling a function from the runtime - // that doesn't have externally-visible side effects. - does_not_affect_tree_shaking: bool = false, - }; - - pub const Comment = struct { text: string }; - - pub const Directive = struct { - value: []const u8, - }; - - pub const ExportClause = struct { - items: []ClauseItem, - is_single_line: bool, - }; - - pub const Empty = struct {}; - - pub const ExportStar = struct { - namespace_ref: Ref, - alias: ?G.ExportStarAlias = null, - import_record_index: u32, - }; - - // This is an "export = value;" statement in TypeScript - pub const ExportEquals = struct { value: ExprNodeIndex }; - - pub const Label = struct { name: LocRef, stmt: StmtNodeIndex }; - - // This is a stand-in for a TypeScript type declaration - pub const TypeScript = struct {}; - - pub const Debugger = struct {}; - - pub const ExportFrom = struct { - items: []ClauseItem, - namespace_ref: Ref, - import_record_index: u32, - is_single_line: bool, - }; - - pub const ExportDefault = struct { - default_name: LocRef, // value may be a SFunction or SClass - value: StmtOrExpr, - - pub fn canBeMoved(self: *const ExportDefault) bool { - return switch (self.value) { - .expr => |e| e.canBeMoved(), - .stmt => |s| switch (s.data) { - .s_class => |class| class.class.canBeMoved(), - .s_function => true, - else => false, - }, - }; - } - }; - - pub const Enum = struct { - name: LocRef, - arg: Ref, - values: []EnumValue, - is_export: bool, - }; - - pub const Namespace = struct { - name: LocRef, - arg: Ref, - stmts: StmtNodeList, - is_export: bool, - }; - - pub const Function = struct { - func: G.Fn, - }; - - pub const Class = struct { class: G.Class, is_export: bool = false }; - - pub const If = struct { - test_: ExprNodeIndex, - yes: StmtNodeIndex, - no: ?StmtNodeIndex, - }; - - pub const For = struct { - // May be a SConst, SLet, SVar, or SExpr - init: ?StmtNodeIndex = null, - test_: ?ExprNodeIndex = null, - update: ?ExprNodeIndex = null, - body: StmtNodeIndex, - }; - - pub const ForIn = struct { - // May be a SConst, SLet, SVar, or SExpr - init: StmtNodeIndex, - value: ExprNodeIndex, - body: StmtNodeIndex, - }; - - pub const ForOf = struct { - is_await: bool = false, - // May be a SConst, SLet, SVar, or SExpr - init: StmtNodeIndex, - value: ExprNodeIndex, - body: StmtNodeIndex, - }; - - pub const DoWhile = struct { body: StmtNodeIndex, test_: ExprNodeIndex }; - - pub const While = struct { - test_: ExprNodeIndex, - body: StmtNodeIndex, - }; - - pub const With = struct { - value: ExprNodeIndex, - body: StmtNodeIndex, - body_loc: logger.Loc = logger.Loc.Empty, - }; - - pub const Try = struct { - body_loc: logger.Loc, - body: StmtNodeList, - - catch_: ?Catch = null, - finally: ?Finally = null, - }; - - pub const Switch = struct { - test_: ExprNodeIndex, - body_loc: logger.Loc, - cases: []Case, - }; - - // This object represents all of these types of import statements: - // - // import 'path' - // import {item1, item2} from 'path' - // import * as ns from 'path' - // import defaultItem, {item1, item2} from 'path' - // import defaultItem, * as ns from 'path' - // - // Many parts are optional and can be combined in different ways. The only - // restriction is that you cannot have both a clause and a star namespace. - pub const Import = struct { - // If this is a star import: This is a Ref for the namespace symbol. The Loc - // for the symbol is StarLoc. - // - // Otherwise: This is an auto-generated Ref for the namespace representing - // the imported file. In this case StarLoc is nil. The NamespaceRef is used - // when converting this module to a CommonJS module. - namespace_ref: Ref, - default_name: ?LocRef = null, - items: []ClauseItem = &.{}, - star_name_loc: ?logger.Loc = null, - import_record_index: u32, - is_single_line: bool = false, - }; - - pub const Return = struct { value: ?ExprNodeIndex = null }; - pub const Throw = struct { value: ExprNodeIndex }; - - pub const Local = struct { - kind: Kind = .k_var, - decls: G.Decl.List = .{}, - is_export: bool = false, - // The TypeScript compiler doesn't generate code for "import foo = bar" - // statements where the import is never used. - was_ts_import_equals: bool = false, - - was_commonjs_export: bool = false, - - pub fn canMergeWith(this: *const Local, other: *const Local) bool { - return this.kind == other.kind and this.is_export == other.is_export and - this.was_commonjs_export == other.was_commonjs_export; - } - - pub const Kind = enum { - k_var, - k_let, - k_const, - k_using, - k_await_using, - - pub fn jsonStringify(self: @This(), writer: anytype) !void { - return try writer.write(@tagName(self)); - } - - pub fn isUsing(self: Kind) bool { - return self == .k_using or self == .k_await_using; - } - - pub fn isReassignable(kind: Kind) bool { - return kind == .k_var or kind == .k_let; - } - }; - }; - - pub const Break = struct { - label: ?LocRef = null, - }; - - pub const Continue = struct { - label: ?LocRef = null, - }; -}; - pub const Catch = struct { loc: logger.Loc, binding: ?BindingNodeIndex = null, @@ -6708,412 +228,11 @@ pub const Finally = struct { pub const Case = struct { loc: logger.Loc, value: ?ExprNodeIndex, body: StmtNodeList }; -pub const Op = struct { - // If you add a new token, remember to add it to "Table" too - pub const Code = enum { - // Prefix - un_pos, // +expr - un_neg, // -expr - un_cpl, // ~expr - un_not, // !expr - un_void, - un_typeof, - un_delete, - - // Prefix update - un_pre_dec, - un_pre_inc, - - // Postfix update - un_post_dec, - un_post_inc, - - /// Left-associative - bin_add, - /// Left-associative - bin_sub, - /// Left-associative - bin_mul, - /// Left-associative - bin_div, - /// Left-associative - bin_rem, - /// Left-associative - bin_pow, - /// Left-associative - bin_lt, - /// Left-associative - bin_le, - /// Left-associative - bin_gt, - /// Left-associative - bin_ge, - /// Left-associative - bin_in, - /// Left-associative - bin_instanceof, - /// Left-associative - bin_shl, - /// Left-associative - bin_shr, - /// Left-associative - bin_u_shr, - /// Left-associative - bin_loose_eq, - /// Left-associative - bin_loose_ne, - /// Left-associative - bin_strict_eq, - /// Left-associative - bin_strict_ne, - /// Left-associative - bin_nullish_coalescing, - /// Left-associative - bin_logical_or, - /// Left-associative - bin_logical_and, - /// Left-associative - bin_bitwise_or, - /// Left-associative - bin_bitwise_and, - /// Left-associative - bin_bitwise_xor, - - /// Non-associative - bin_comma, - - /// Right-associative - bin_assign, - /// Right-associative - bin_add_assign, - /// Right-associative - bin_sub_assign, - /// Right-associative - bin_mul_assign, - /// Right-associative - bin_div_assign, - /// Right-associative - bin_rem_assign, - /// Right-associative - bin_pow_assign, - /// Right-associative - bin_shl_assign, - /// Right-associative - bin_shr_assign, - /// Right-associative - bin_u_shr_assign, - /// Right-associative - bin_bitwise_or_assign, - /// Right-associative - bin_bitwise_and_assign, - /// Right-associative - bin_bitwise_xor_assign, - /// Right-associative - bin_nullish_coalescing_assign, - /// Right-associative - bin_logical_or_assign, - /// Right-associative - bin_logical_and_assign, - - pub fn jsonStringify(self: @This(), writer: anytype) !void { - return try writer.write(@tagName(self)); - } - - pub fn unaryAssignTarget(code: Op.Code) AssignTarget { - if (@intFromEnum(code) >= - @intFromEnum(Op.Code.un_pre_dec) and @intFromEnum(code) <= - @intFromEnum(Op.Code.un_post_inc)) - { - return AssignTarget.update; - } - - return AssignTarget.none; - } - pub fn isLeftAssociative(code: Op.Code) bool { - return @intFromEnum(code) >= - @intFromEnum(Op.Code.bin_add) and - @intFromEnum(code) < @intFromEnum(Op.Code.bin_comma) and code != .bin_pow; - } - pub fn isRightAssociative(code: Op.Code) bool { - return @intFromEnum(code) >= @intFromEnum(Op.Code.bin_assign) or code == .bin_pow; - } - pub fn binaryAssignTarget(code: Op.Code) AssignTarget { - if (code == .bin_assign) { - return AssignTarget.replace; - } - - if (@intFromEnum(code) > @intFromEnum(Op.Code.bin_assign)) { - return AssignTarget.update; - } - - return AssignTarget.none; - } - - pub fn isPrefix(code: Op.Code) bool { - return @intFromEnum(code) < @intFromEnum(Op.Code.un_post_dec); - } - }; - - pub const Level = enum(u6) { - lowest, - comma, - spread, - yield, - assign, - conditional, - nullish_coalescing, - logical_or, - logical_and, - bitwise_or, - bitwise_xor, - bitwise_and, - equals, - compare, - shift, - add, - multiply, - exponentiation, - prefix, - postfix, - new, - call, - member, - - pub inline fn lt(self: Level, b: Level) bool { - return @intFromEnum(self) < @intFromEnum(b); - } - pub inline fn gt(self: Level, b: Level) bool { - return @intFromEnum(self) > @intFromEnum(b); - } - pub inline fn gte(self: Level, b: Level) bool { - return @intFromEnum(self) >= @intFromEnum(b); - } - pub inline fn lte(self: Level, b: Level) bool { - return @intFromEnum(self) <= @intFromEnum(b); - } - pub inline fn eql(self: Level, b: Level) bool { - return @intFromEnum(self) == @intFromEnum(b); - } - - pub inline fn sub(self: Level, i: anytype) Level { - return @as(Level, @enumFromInt(@intFromEnum(self) - i)); - } - - pub inline fn addF(self: Level, i: anytype) Level { - return @as(Level, @enumFromInt(@intFromEnum(self) + i)); - } - }; - - text: string, - level: Level, - is_keyword: bool = false, - - pub fn init(triple: anytype) Op { - return Op{ - .text = triple.@"0", - .level = triple.@"1", - .is_keyword = triple.@"2", - }; - } - - pub fn jsonStringify(self: *const @This(), writer: anytype) !void { - return try writer.write(self.text); - } - - pub const TableType: std.EnumArray(Op.Code, Op) = undefined; - pub const Table = brk: { - var table = std.EnumArray(Op.Code, Op).initUndefined(); - - // Prefix - table.set(Op.Code.un_pos, Op.init(.{ "+", Level.prefix, false })); - table.set(Op.Code.un_neg, Op.init(.{ "-", Level.prefix, false })); - table.set(Op.Code.un_cpl, Op.init(.{ "~", Level.prefix, false })); - table.set(Op.Code.un_not, Op.init(.{ "!", Level.prefix, false })); - table.set(Op.Code.un_void, Op.init(.{ "void", Level.prefix, true })); - table.set(Op.Code.un_typeof, Op.init(.{ "typeof", Level.prefix, true })); - table.set(Op.Code.un_delete, Op.init(.{ "delete", Level.prefix, true })); - - // Prefix update - table.set(Op.Code.un_pre_dec, Op.init(.{ "--", Level.prefix, false })); - table.set(Op.Code.un_pre_inc, Op.init(.{ "++", Level.prefix, false })); - - // Postfix update - table.set(Op.Code.un_post_dec, Op.init(.{ "--", Level.postfix, false })); - table.set(Op.Code.un_post_inc, Op.init(.{ "++", Level.postfix, false })); - - // Left-associative - table.set(Op.Code.bin_add, Op.init(.{ "+", Level.add, false })); - table.set(Op.Code.bin_sub, Op.init(.{ "-", Level.add, false })); - table.set(Op.Code.bin_mul, Op.init(.{ "*", Level.multiply, false })); - table.set(Op.Code.bin_div, Op.init(.{ "/", Level.multiply, false })); - table.set(Op.Code.bin_rem, Op.init(.{ "%", Level.multiply, false })); - table.set(Op.Code.bin_pow, Op.init(.{ "**", Level.exponentiation, false })); - table.set(Op.Code.bin_lt, Op.init(.{ "<", Level.compare, false })); - table.set(Op.Code.bin_le, Op.init(.{ "<=", Level.compare, false })); - table.set(Op.Code.bin_gt, Op.init(.{ ">", Level.compare, false })); - table.set(Op.Code.bin_ge, Op.init(.{ ">=", Level.compare, false })); - table.set(Op.Code.bin_in, Op.init(.{ "in", Level.compare, true })); - table.set(Op.Code.bin_instanceof, Op.init(.{ "instanceof", Level.compare, true })); - table.set(Op.Code.bin_shl, Op.init(.{ "<<", Level.shift, false })); - table.set(Op.Code.bin_shr, Op.init(.{ ">>", Level.shift, false })); - table.set(Op.Code.bin_u_shr, Op.init(.{ ">>>", Level.shift, false })); - table.set(Op.Code.bin_loose_eq, Op.init(.{ "==", Level.equals, false })); - table.set(Op.Code.bin_loose_ne, Op.init(.{ "!=", Level.equals, false })); - table.set(Op.Code.bin_strict_eq, Op.init(.{ "===", Level.equals, false })); - table.set(Op.Code.bin_strict_ne, Op.init(.{ "!==", Level.equals, false })); - table.set(Op.Code.bin_nullish_coalescing, Op.init(.{ "??", Level.nullish_coalescing, false })); - table.set(Op.Code.bin_logical_or, Op.init(.{ "||", Level.logical_or, false })); - table.set(Op.Code.bin_logical_and, Op.init(.{ "&&", Level.logical_and, false })); - table.set(Op.Code.bin_bitwise_or, Op.init(.{ "|", Level.bitwise_or, false })); - table.set(Op.Code.bin_bitwise_and, Op.init(.{ "&", Level.bitwise_and, false })); - table.set(Op.Code.bin_bitwise_xor, Op.init(.{ "^", Level.bitwise_xor, false })); - - // Non-associative - table.set(Op.Code.bin_comma, Op.init(.{ ",", Level.comma, false })); - - // Right-associative - table.set(Op.Code.bin_assign, Op.init(.{ "=", Level.assign, false })); - table.set(Op.Code.bin_add_assign, Op.init(.{ "+=", Level.assign, false })); - table.set(Op.Code.bin_sub_assign, Op.init(.{ "-=", Level.assign, false })); - table.set(Op.Code.bin_mul_assign, Op.init(.{ "*=", Level.assign, false })); - table.set(Op.Code.bin_div_assign, Op.init(.{ "/=", Level.assign, false })); - table.set(Op.Code.bin_rem_assign, Op.init(.{ "%=", Level.assign, false })); - table.set(Op.Code.bin_pow_assign, Op.init(.{ "**=", Level.assign, false })); - table.set(Op.Code.bin_shl_assign, Op.init(.{ "<<=", Level.assign, false })); - table.set(Op.Code.bin_shr_assign, Op.init(.{ ">>=", Level.assign, false })); - table.set(Op.Code.bin_u_shr_assign, Op.init(.{ ">>>=", Level.assign, false })); - table.set(Op.Code.bin_bitwise_or_assign, Op.init(.{ "|=", Level.assign, false })); - table.set(Op.Code.bin_bitwise_and_assign, Op.init(.{ "&=", Level.assign, false })); - table.set(Op.Code.bin_bitwise_xor_assign, Op.init(.{ "^=", Level.assign, false })); - table.set(Op.Code.bin_nullish_coalescing_assign, Op.init(.{ "??=", Level.assign, false })); - table.set(Op.Code.bin_logical_or_assign, Op.init(.{ "||=", Level.assign, false })); - table.set(Op.Code.bin_logical_and_assign, Op.init(.{ "&&=", Level.assign, false })); - - break :brk table; - }; -}; - pub const ArrayBinding = struct { binding: BindingNodeIndex, default_value: ?ExprNodeIndex = null, }; -pub const Ast = struct { - pub const TopLevelSymbolToParts = std.ArrayHashMapUnmanaged(Ref, BabyList(u32), Ref.ArrayHashCtx, false); - - approximate_newline_count: usize = 0, - has_lazy_export: bool = false, - runtime_imports: Runtime.Imports = .{}, - - nested_scope_slot_counts: SlotCounts = SlotCounts{}, - - runtime_import_record_id: ?u32 = null, - needs_runtime: bool = false, - // This is a list of CommonJS features. When a file uses CommonJS features, - // it's not a candidate for "flat bundling" and must be wrapped in its own - // closure. - has_top_level_return: bool = false, - uses_exports_ref: bool = false, - uses_module_ref: bool = false, - uses_require_ref: bool = false, - commonjs_module_exports_assigned_deoptimized: bool = false, - - force_cjs_to_esm: bool = false, - exports_kind: ExportsKind = ExportsKind.none, - - // This is a list of ES6 features. They are ranges instead of booleans so - // that they can be used in log messages. Check to see if "Len > 0". - import_keyword: logger.Range = logger.Range.None, // Does not include TypeScript-specific syntax or "import()" - export_keyword: logger.Range = logger.Range.None, // Does not include TypeScript-specific syntax - top_level_await_keyword: logger.Range = logger.Range.None, - - /// These are stored at the AST level instead of on individual AST nodes so - /// they can be manipulated efficiently without a full AST traversal - import_records: ImportRecord.List = .{}, - - hashbang: string = "", - directive: ?string = null, - parts: Part.List = Part.List{}, - // This list may be mutated later, so we should store the capacity - symbols: Symbol.List = Symbol.List{}, - module_scope: Scope = Scope{}, - char_freq: ?CharFreq = null, - exports_ref: Ref = Ref.None, - module_ref: Ref = Ref.None, - /// When using format .bake_internal_dev, this is the HMR variable instead - /// of the wrapper. This is because that format does not store module - /// wrappers in a variable. - wrapper_ref: Ref = Ref.None, - require_ref: Ref = Ref.None, - - // These are used when bundling. They are filled in during the parser pass - // since we already have to traverse the AST then anyway and the parser pass - // is conveniently fully parallelized. - named_imports: NamedImports = .{}, - named_exports: NamedExports = .{}, - export_star_import_records: []u32 = &([_]u32{}), - - // allocator: std.mem.Allocator, - top_level_symbols_to_parts: TopLevelSymbolToParts = .{}, - - commonjs_named_exports: CommonJSNamedExports = .{}, - - redirect_import_record_index: ?u32 = null, - - /// Only populated when bundling - target: bun.options.Target = .browser, - // const_values: ConstValuesMap = .{}, - ts_enums: TsEnumsMap = .{}, - - /// Not to be confused with `commonjs_named_exports` - /// This is a list of named exports that may exist in a CommonJS module - /// We use this with `commonjs_at_runtime` to re-export CommonJS - has_commonjs_export_names: bool = false, - import_meta_ref: Ref = Ref.None, - - pub const CommonJSNamedExport = struct { - loc_ref: LocRef, - needs_decl: bool = true, - }; - pub const CommonJSNamedExports = bun.StringArrayHashMapUnmanaged(CommonJSNamedExport); - - pub const NamedImports = std.ArrayHashMapUnmanaged(Ref, NamedImport, RefHashCtx, true); - pub const NamedExports = bun.StringArrayHashMapUnmanaged(NamedExport); - pub const ConstValuesMap = std.ArrayHashMapUnmanaged(Ref, Expr, RefHashCtx, false); - pub const TsEnumsMap = std.ArrayHashMapUnmanaged(Ref, bun.StringHashMapUnmanaged(InlinedEnumValue), RefHashCtx, false); - - pub fn fromParts(parts: []Part) Ast { - return Ast{ - .parts = Part.List.init(parts), - .runtime_imports = .{}, - }; - } - - pub fn initTest(parts: []Part) Ast { - return Ast{ - .parts = Part.List.init(parts), - .runtime_imports = .{}, - }; - } - - pub const empty = Ast{ .parts = Part.List{}, .runtime_imports = .{} }; - - pub fn toJSON(self: *const Ast, _: std.mem.Allocator, stream: anytype) !void { - const opts = std.json.StringifyOptions{ .whitespace = std.json.StringifyOptions.Whitespace{ - .separator = true, - } }; - try std.json.stringify(self.parts, opts, stream); - } - - /// Do not call this if it wasn't globally allocated! - pub fn deinit(this: *Ast) void { - // TODO: assert mimalloc-owned memory - if (this.parts.len > 0) this.parts.deinitWithAllocator(bun.default_allocator); - if (this.symbols.len > 0) this.symbols.deinitWithAllocator(bun.default_allocator); - if (this.import_records.len > 0) this.import_records.deinitWithAllocator(bun.default_allocator); - } -}; - /// TLA => Top Level Await pub const TlaCheck = struct { depth: u32 = 0, @@ -7121,349 +240,11 @@ pub const TlaCheck = struct { import_record_index: Index.Int = Index.invalid.get(), }; -/// Like Ast but slimmer and for bundling only. -/// -/// On Linux, the hottest function in the bundler is: -/// src.multi_array_list.MultiArrayList(src.js_ast.Ast).ensureTotalCapacity -/// https://share.firefox.dev/3NNlRKt -/// -/// So we make a slimmer version of Ast for bundling that doesn't allocate as much memory -pub const BundledAst = struct { - approximate_newline_count: u32 = 0, - nested_scope_slot_counts: SlotCounts = .{}, - - exports_kind: ExportsKind = .none, - - /// These are stored at the AST level instead of on individual AST nodes so - /// they can be manipulated efficiently without a full AST traversal - import_records: ImportRecord.List = .{}, - - hashbang: string = "", - parts: Part.List = .{}, - css: ?*bun.css.BundlerStyleSheet = null, - url_for_css: []const u8 = "", - symbols: Symbol.List = .{}, - module_scope: Scope = .{}, - char_freq: CharFreq = undefined, - exports_ref: Ref = Ref.None, - module_ref: Ref = Ref.None, - wrapper_ref: Ref = Ref.None, - require_ref: Ref = Ref.None, - top_level_await_keyword: logger.Range, - tla_check: TlaCheck = .{}, - - // These are used when bundling. They are filled in during the parser pass - // since we already have to traverse the AST then anyway and the parser pass - // is conveniently fully parallelized. - named_imports: NamedImports = .{}, - named_exports: NamedExports = .{}, - export_star_import_records: []u32 = &.{}, - - top_level_symbols_to_parts: TopLevelSymbolToParts = .{}, - - commonjs_named_exports: CommonJSNamedExports = .{}, - - redirect_import_record_index: u32 = std.math.maxInt(u32), - - /// Only populated when bundling. When --server-components is passed, this - /// will be .browser when it is a client component, and the server's target - /// on the server. - target: bun.options.Target = .browser, - - // const_values: ConstValuesMap = .{}, - ts_enums: Ast.TsEnumsMap = .{}, - - flags: BundledAst.Flags = .{}, - - pub const NamedImports = Ast.NamedImports; - pub const NamedExports = Ast.NamedExports; - pub const TopLevelSymbolToParts = Ast.TopLevelSymbolToParts; - pub const CommonJSNamedExports = Ast.CommonJSNamedExports; - pub const ConstValuesMap = Ast.ConstValuesMap; - - pub const Flags = packed struct(u8) { - // This is a list of CommonJS features. When a file uses CommonJS features, - // it's not a candidate for "flat bundling" and must be wrapped in its own - // closure. - uses_exports_ref: bool = false, - uses_module_ref: bool = false, - // uses_require_ref: bool = false, - uses_export_keyword: bool = false, - has_char_freq: bool = false, - force_cjs_to_esm: bool = false, - has_lazy_export: bool = false, - commonjs_module_exports_assigned_deoptimized: bool = false, - has_explicit_use_strict_directive: bool = false, - }; - - pub const empty = BundledAst.init(Ast.empty); - - pub fn toAST(this: *const BundledAst) Ast { - return .{ - .approximate_newline_count = this.approximate_newline_count, - .nested_scope_slot_counts = this.nested_scope_slot_counts, - - .exports_kind = this.exports_kind, - - .import_records = this.import_records, - - .hashbang = this.hashbang, - .parts = this.parts, - // This list may be mutated later, so we should store the capacity - .symbols = this.symbols, - .module_scope = this.module_scope, - .char_freq = if (this.flags.has_char_freq) this.char_freq else null, - .exports_ref = this.exports_ref, - .module_ref = this.module_ref, - .wrapper_ref = this.wrapper_ref, - .require_ref = this.require_ref, - .top_level_await_keyword = this.top_level_await_keyword, - - // These are used when bundling. They are filled in during the parser pass - // since we already have to traverse the AST then anyway and the parser pass - // is conveniently fully parallelized. - .named_imports = this.named_imports, - .named_exports = this.named_exports, - .export_star_import_records = this.export_star_import_records, - - .top_level_symbols_to_parts = this.top_level_symbols_to_parts, - - .commonjs_named_exports = this.commonjs_named_exports, - - .redirect_import_record_index = this.redirect_import_record_index, - - .target = this.target, - - // .const_values = this.const_values, - .ts_enums = this.ts_enums, - - .uses_exports_ref = this.flags.uses_exports_ref, - .uses_module_ref = this.flags.uses_module_ref, - // .uses_require_ref = ast.uses_require_ref, - .export_keyword = .{ .len = if (this.flags.uses_export_keyword) 1 else 0, .loc = .{} }, - .force_cjs_to_esm = this.flags.force_cjs_to_esm, - .has_lazy_export = this.flags.has_lazy_export, - .commonjs_module_exports_assigned_deoptimized = this.flags.commonjs_module_exports_assigned_deoptimized, - .directive = if (this.flags.has_explicit_use_strict_directive) "use strict" else null, - }; - } - - pub fn init(ast: Ast) BundledAst { - return .{ - .approximate_newline_count = @as(u32, @truncate(ast.approximate_newline_count)), - .nested_scope_slot_counts = ast.nested_scope_slot_counts, - - .exports_kind = ast.exports_kind, - - .import_records = ast.import_records, - - .hashbang = ast.hashbang, - .parts = ast.parts, - // This list may be mutated later, so we should store the capacity - .symbols = ast.symbols, - .module_scope = ast.module_scope, - .char_freq = ast.char_freq orelse undefined, - .exports_ref = ast.exports_ref, - .module_ref = ast.module_ref, - .wrapper_ref = ast.wrapper_ref, - .require_ref = ast.require_ref, - .top_level_await_keyword = ast.top_level_await_keyword, - // These are used when bundling. They are filled in during the parser pass - // since we already have to traverse the AST then anyway and the parser pass - // is conveniently fully parallelized. - .named_imports = ast.named_imports, - .named_exports = ast.named_exports, - .export_star_import_records = ast.export_star_import_records, - - // .allocator = ast.allocator, - .top_level_symbols_to_parts = ast.top_level_symbols_to_parts, - - .commonjs_named_exports = ast.commonjs_named_exports, - - .redirect_import_record_index = ast.redirect_import_record_index orelse std.math.maxInt(u32), - - .target = ast.target, - - // .const_values = ast.const_values, - .ts_enums = ast.ts_enums, - - .flags = .{ - .uses_exports_ref = ast.uses_exports_ref, - .uses_module_ref = ast.uses_module_ref, - // .uses_require_ref = ast.uses_require_ref, - .uses_export_keyword = ast.export_keyword.len > 0, - .has_char_freq = ast.char_freq != null, - .force_cjs_to_esm = ast.force_cjs_to_esm, - .has_lazy_export = ast.has_lazy_export, - .commonjs_module_exports_assigned_deoptimized = ast.commonjs_module_exports_assigned_deoptimized, - .has_explicit_use_strict_directive = strings.eqlComptime(ast.directive orelse "", "use strict"), - }, - }; - } - - /// TODO: Move this from being done on all parse tasks into the start of the linker. This currently allocates base64 encoding for every small file loaded thing. - pub fn addUrlForCss( - this: *BundledAst, - allocator: std.mem.Allocator, - source: *const logger.Source, - mime_type_: ?[]const u8, - unique_key: ?[]const u8, - ) void { - { - const mime_type = if (mime_type_) |m| m else MimeType.byExtension(bun.strings.trimLeadingChar(std.fs.path.extension(source.path.text), '.')).value; - const contents = source.contents; - // TODO: make this configurable - const COPY_THRESHOLD = 128 * 1024; // 128kb - const should_copy = contents.len >= COPY_THRESHOLD and unique_key != null; - if (should_copy) return; - this.url_for_css = url_for_css: { - - // Encode as base64 - const encode_len = bun.base64.encodeLen(contents); - const data_url_prefix_len = "data:".len + mime_type.len + ";base64,".len; - const total_buffer_len = data_url_prefix_len + encode_len; - var encoded = allocator.alloc(u8, total_buffer_len) catch bun.outOfMemory(); - _ = std.fmt.bufPrint(encoded[0..data_url_prefix_len], "data:{s};base64,", .{mime_type}) catch unreachable; - const len = bun.base64.encode(encoded[data_url_prefix_len..], contents); - break :url_for_css encoded[0 .. data_url_prefix_len + len]; - }; - } - } -}; - pub const Span = struct { text: string = "", range: logger.Range = .{}, }; -/// This is for TypeScript "enum" and "namespace" blocks. Each block can -/// potentially be instantiated multiple times. The exported members of each -/// block are merged into a single namespace while the non-exported code is -/// still scoped to just within that block: -/// -/// let x = 1; -/// namespace Foo { -/// let x = 2; -/// export let y = 3; -/// } -/// namespace Foo { -/// console.log(x); // 1 -/// console.log(y); // 3 -/// } -/// -/// Doing this also works inside an enum: -/// -/// enum Foo { -/// A = 3, -/// B = A + 1, -/// } -/// enum Foo { -/// C = A + 2, -/// } -/// console.log(Foo.B) // 4 -/// console.log(Foo.C) // 5 -/// -/// This is a form of identifier lookup that works differently than the -/// hierarchical scope-based identifier lookup in JavaScript. Lookup now needs -/// to search sibling scopes in addition to parent scopes. This is accomplished -/// by sharing the map of exported members between all matching sibling scopes. -pub const TSNamespaceScope = struct { - /// This is specific to this namespace block. It's the argument of the - /// immediately-invoked function expression that the namespace block is - /// compiled into: - /// - /// var ns; - /// (function (ns2) { - /// ns2.x = 123; - /// })(ns || (ns = {})); - /// - /// This variable is "ns2" in the above example. It's the symbol to use when - /// generating property accesses off of this namespace when it's in scope. - arg_ref: Ref, - - /// This is shared between all sibling namespace blocks - exported_members: *TSNamespaceMemberMap, - - /// This is a lazily-generated map of identifiers that actually represent - /// property accesses to this namespace's properties. For example: - /// - /// namespace x { - /// export let y = 123 - /// } - /// namespace x { - /// export let z = y - /// } - /// - /// This should be compiled into the following code: - /// - /// var x; - /// (function(x2) { - /// x2.y = 123; - /// })(x || (x = {})); - /// (function(x3) { - /// x3.z = x3.y; - /// })(x || (x = {})); - /// - /// When we try to find the symbol "y", we instead return one of these lazily - /// generated proxy symbols that represent the property access "x3.y". This - /// map is unique per namespace block because "x3" is the argument symbol that - /// is specific to that particular namespace block. - property_accesses: bun.StringArrayHashMapUnmanaged(Ref) = .{}, - - /// Even though enums are like namespaces and both enums and namespaces allow - /// implicit references to properties of sibling scopes, they behave like - /// separate, er, namespaces. Implicit references only work namespace-to- - /// namespace and enum-to-enum. They do not work enum-to-namespace. And I'm - /// not sure what's supposed to happen for the namespace-to-enum case because - /// the compiler crashes: https://github.com/microsoft/TypeScript/issues/46891. - /// So basically these both work: - /// - /// enum a { b = 1 } - /// enum a { c = b } - /// - /// namespace x { export let y = 1 } - /// namespace x { export let z = y } - /// - /// This doesn't work: - /// - /// enum a { b = 1 } - /// namespace a { export let c = b } - /// - /// And this crashes the TypeScript compiler: - /// - /// namespace a { export let b = 1 } - /// enum a { c = b } - /// - /// Therefore we only allow enum/enum and namespace/namespace interactions. - is_enum_scope: bool, -}; - -pub const TSNamespaceMemberMap = bun.StringArrayHashMapUnmanaged(TSNamespaceMember); - -pub const TSNamespaceMember = struct { - loc: logger.Loc, - data: Data, - - pub const Data = union(enum) { - /// "namespace ns { export let it }" - property, - /// "namespace ns { export namespace it {} }" - namespace: *TSNamespaceMemberMap, - /// "enum ns { it }" - enum_number: f64, - /// "enum ns { it = 'it' }" - enum_string: *E.String, - /// "enum ns { it = something() }" - enum_property: void, - - pub fn isEnum(data: Data) bool { - return switch (data) { - inline else => |_, tag| comptime std.mem.startsWith(u8, @tagName(tag), "enum_"), - }; - } - }; -}; - /// Inlined enum values can only be numbers and strings /// This type special cases an encoding similar to JSValue, where nan-boxing is used /// to encode both a 64-bit pointer or a 64-bit float using 64 bits. @@ -7802,1327 +583,13 @@ pub const StrictModeKind = enum(u4) { } }; -pub const Scope = struct { - pub const MemberHashMap = bun.StringHashMapUnmanaged(Member); - - id: usize = 0, - kind: Kind = Kind.block, - parent: ?*Scope = null, - children: BabyList(*Scope) = .{}, - members: MemberHashMap = .{}, - generated: BabyList(Ref) = .{}, - - // This is used to store the ref of the label symbol for ScopeLabel scopes. - label_ref: ?Ref = null, - label_stmt_is_loop: bool = false, - - // If a scope contains a direct eval() expression, then none of the symbols - // inside that scope can be renamed. We conservatively assume that the - // evaluated code might reference anything that it has access to. - contains_direct_eval: bool = false, - - // This is to help forbid "arguments" inside class body scopes - forbid_arguments: bool = false, - - strict_mode: StrictModeKind = StrictModeKind.sloppy_mode, - - is_after_const_local_prefix: bool = false, - - // This will be non-null if this is a TypeScript "namespace" or "enum" - ts_namespace: ?*TSNamespaceScope = null, - - pub const NestedScopeMap = std.AutoArrayHashMap(u32, bun.BabyList(*Scope)); - - pub fn getMemberHash(name: []const u8) u64 { - return bun.StringHashMapContext.hash(.{}, name); - } - - pub fn getMemberWithHash(this: *const Scope, name: []const u8, hash_value: u64) ?Member { - const hashed = bun.StringHashMapContext.Prehashed{ - .value = hash_value, - .input = name, - }; - return this.members.getAdapted(name, hashed); - } - - pub fn getOrPutMemberWithHash( - this: *Scope, - allocator: std.mem.Allocator, - name: []const u8, - hash_value: u64, - ) !MemberHashMap.GetOrPutResult { - const hashed = bun.StringHashMapContext.Prehashed{ - .value = hash_value, - .input = name, - }; - return this.members.getOrPutContextAdapted(allocator, name, hashed, .{}); - } - - pub fn reset(this: *Scope) void { - this.children.clearRetainingCapacity(); - this.generated.clearRetainingCapacity(); - this.members.clearRetainingCapacity(); - this.parent = null; - this.id = 0; - this.label_ref = null; - this.label_stmt_is_loop = false; - this.contains_direct_eval = false; - this.strict_mode = .sloppy_mode; - this.kind = .block; - } - - // Do not make this a packed struct - // Two hours of debugging time lost to that. - // It causes a crash due to undefined memory - pub const Member = struct { - ref: Ref, - loc: logger.Loc, - - pub fn eql(a: Member, b: Member) bool { - return @call(bun.callmod_inline, Ref.eql, .{ a.ref, b.ref }) and a.loc.start == b.loc.start; - } - }; - - pub const SymbolMergeResult = enum { - forbidden, - replace_with_new, - overwrite_with_new, - keep_existing, - become_private_get_set_pair, - become_private_static_get_set_pair, - }; - - pub fn canMergeSymbols( - scope: *Scope, - existing: Symbol.Kind, - new: Symbol.Kind, - comptime is_typescript_enabled: bool, - ) SymbolMergeResult { - if (existing == .unbound) { - return .replace_with_new; - } - - if (comptime is_typescript_enabled) { - // In TypeScript, imports are allowed to silently collide with symbols within - // the module. Presumably this is because the imports may be type-only: - // - // import {Foo} from 'bar' - // class Foo {} - // - if (existing == .import) { - return .replace_with_new; - } - - // "enum Foo {} enum Foo {}" - // "namespace Foo { ... } enum Foo {}" - if (new == .ts_enum and (existing == .ts_enum or existing == .ts_namespace)) { - return .replace_with_new; - } - - // "namespace Foo { ... } namespace Foo { ... }" - // "function Foo() {} namespace Foo { ... }" - // "enum Foo {} namespace Foo { ... }" - if (new == .ts_namespace) { - switch (existing) { - .ts_namespace, - .ts_enum, - .hoisted_function, - .generator_or_async_function, - .class, - => return .keep_existing, - else => {}, - } - } - } - - // "var foo; var foo;" - // "var foo; function foo() {}" - // "function foo() {} var foo;" - // "function *foo() {} function *foo() {}" but not "{ function *foo() {} function *foo() {} }" - if (Symbol.isKindHoistedOrFunction(new) and - Symbol.isKindHoistedOrFunction(existing) and - (scope.kind == .entry or scope.kind == .function_body or scope.kind == .function_args or - (new == existing and Symbol.isKindHoisted(existing)))) - { - return .replace_with_new; - } - - // "get #foo() {} set #foo() {}" - // "set #foo() {} get #foo() {}" - if ((existing == .private_get and new == .private_set) or - (existing == .private_set and new == .private_get)) - { - return .become_private_get_set_pair; - } - if ((existing == .private_static_get and new == .private_static_set) or - (existing == .private_static_set and new == .private_static_get)) - { - return .become_private_static_get_set_pair; - } - - // "try {} catch (e) { var e }" - if (existing == .catch_identifier and new == .hoisted) { - return .replace_with_new; - } - - // "function() { var arguments }" - if (existing == .arguments and new == .hoisted) { - return .keep_existing; - } - - // "function() { let arguments }" - if (existing == .arguments and new != .hoisted) { - return .overwrite_with_new; - } - - return .forbidden; - } - - pub const Kind = enum(u8) { - block, - with, - label, - class_name, - class_body, - catch_binding, - - // The scopes below stop hoisted variables from extending into parent scopes - entry, // This is a module, TypeScript enum, or TypeScript namespace - function_args, - function_body, - class_static_init, - - pub fn jsonStringify(self: @This(), writer: anytype) !void { - return try writer.write(@tagName(self)); - } - }; - - pub fn recursiveSetStrictMode(s: *Scope, kind: StrictModeKind) void { - if (s.strict_mode == .sloppy_mode) { - s.strict_mode = kind; - for (s.children.slice()) |child| { - child.recursiveSetStrictMode(kind); - } - } - } - - pub inline fn kindStopsHoisting(s: *const Scope) bool { - return @intFromEnum(s.kind) >= @intFromEnum(Kind.entry); - } -}; - pub fn printmem(comptime format: string, args: anytype) void { defer Output.flush(); Output.initTest(); Output.print(format, args); } -pub const Macro = struct { - const JavaScript = bun.JSC; - const Resolver = @import("./resolver/resolver.zig").Resolver; - const isPackagePath = @import("./resolver/resolver.zig").isPackagePath; - const ResolveResult = @import("./resolver/resolver.zig").Result; - const DotEnv = @import("./env_loader.zig"); - const js = @import("./bun.js/javascript_core_c_api.zig"); - const Transpiler = bun.Transpiler; - const MacroEntryPoint = bun.transpiler.EntryPoints.MacroEntryPoint; - const MacroRemap = @import("./resolver/package_json.zig").MacroMap; - pub const MacroRemapEntry = @import("./resolver/package_json.zig").MacroImportReplacementMap; - - pub const namespace: string = "macro"; - pub const namespaceWithColon: string = namespace ++ ":"; - - pub fn isMacroPath(str: string) bool { - return strings.hasPrefixComptime(str, namespaceWithColon); - } - - pub const MacroContext = struct { - pub const MacroMap = std.AutoArrayHashMap(i32, Macro); - - resolver: *Resolver, - env: *DotEnv.Loader, - macros: MacroMap, - remap: MacroRemap, - javascript_object: JSC.JSValue = JSC.JSValue.zero, - - pub fn getRemap(this: MacroContext, path: string) ?MacroRemapEntry { - if (this.remap.entries.len == 0) return null; - return this.remap.get(path); - } - - pub fn init(transpiler: *Transpiler) MacroContext { - return MacroContext{ - .macros = MacroMap.init(default_allocator), - .resolver = &transpiler.resolver, - .env = transpiler.env, - .remap = transpiler.options.macro_remap, - }; - } - - pub fn call( - this: *MacroContext, - import_record_path: string, - source_dir: string, - log: *logger.Log, - source: *const logger.Source, - import_range: logger.Range, - caller: Expr, - function_name: string, - ) anyerror!Expr { - Expr.Data.Store.disable_reset = true; - Stmt.Data.Store.disable_reset = true; - defer Expr.Data.Store.disable_reset = false; - defer Stmt.Data.Store.disable_reset = false; - // const is_package_path = isPackagePath(specifier); - const import_record_path_without_macro_prefix = if (isMacroPath(import_record_path)) - import_record_path[namespaceWithColon.len..] - else - import_record_path; - - bun.assert(!isMacroPath(import_record_path_without_macro_prefix)); - - const input_specifier = brk: { - if (JSC.ModuleLoader.HardcodedModule.Alias.get(import_record_path, .bun)) |replacement| { - break :brk replacement.path; - } - - const resolve_result = this.resolver.resolve(source_dir, import_record_path_without_macro_prefix, .stmt) catch |err| { - switch (err) { - error.ModuleNotFound => { - log.addResolveError( - source, - import_range, - log.msgs.allocator, - "Macro \"{s}\" not found", - .{import_record_path}, - .stmt, - err, - ) catch unreachable; - return error.MacroNotFound; - }, - else => { - log.addRangeErrorFmt( - source, - import_range, - log.msgs.allocator, - "{s} resolving macro \"{s}\"", - .{ @errorName(err), import_record_path }, - ) catch unreachable; - return err; - }, - } - }; - break :brk resolve_result.path_pair.primary.text; - }; - - var specifier_buf: [64]u8 = undefined; - var specifier_buf_len: u32 = 0; - const hash = MacroEntryPoint.generateID( - input_specifier, - function_name, - &specifier_buf, - &specifier_buf_len, - ); - - const macro_entry = this.macros.getOrPut(hash) catch unreachable; - if (!macro_entry.found_existing) { - macro_entry.value_ptr.* = Macro.init( - default_allocator, - this.resolver, - input_specifier, - log, - this.env, - function_name, - specifier_buf[0..specifier_buf_len], - hash, - ) catch |err| { - macro_entry.value_ptr.* = Macro{ .resolver = undefined, .disabled = true }; - return err; - }; - Output.flush(); - } - defer Output.flush(); - - const macro = macro_entry.value_ptr.*; - if (macro.disabled) { - return caller; - } - macro.vm.enableMacroMode(); - defer macro.vm.disableMacroMode(); - - const Wrapper = struct { - args: std.meta.ArgsTuple(@TypeOf(Macro.Runner.run)), - ret: Runner.MacroError!Expr, - - pub fn call(self: *@This()) void { - self.ret = @call(.auto, Macro.Runner.run, self.args); - } - }; - var wrapper = Wrapper{ - .args = .{ - macro, - log, - default_allocator, - function_name, - caller, - source, - hash, - this.javascript_object, - }, - .ret = undefined, - }; - - macro.vm.runWithAPILock(Wrapper, &wrapper, Wrapper.call); - return try wrapper.ret; - // this.macros.getOrPut(key: K) - } - }; - - pub const MacroResult = struct { - import_statements: []S.Import = &[_]S.Import{}, - replacement: Expr, - }; - - resolver: *Resolver, - vm: *JavaScript.VirtualMachine = undefined, - - resolved: ResolveResult = undefined, - disabled: bool = false, - - pub fn init( - _: std.mem.Allocator, - resolver: *Resolver, - input_specifier: []const u8, - log: *logger.Log, - env: *DotEnv.Loader, - function_name: string, - specifier: string, - hash: i32, - ) !Macro { - var vm: *JavaScript.VirtualMachine = if (JavaScript.VirtualMachine.isLoaded()) - JavaScript.VirtualMachine.get() - else brk: { - const old_transform_options = resolver.opts.transform_options; - defer resolver.opts.transform_options = old_transform_options; - - // JSC needs to be initialized if building from CLI - JSC.initialize(false); - - var _vm = try JavaScript.VirtualMachine.init(.{ - .allocator = default_allocator, - .args = resolver.opts.transform_options, - .log = log, - .is_main_thread = false, - .env_loader = env, - }); - - _vm.enableMacroMode(); - _vm.eventLoop().ensureWaker(); - - try _vm.transpiler.configureDefines(); - break :brk _vm; - }; - - vm.enableMacroMode(); - - const loaded_result = try vm.loadMacroEntryPoint(input_specifier, function_name, specifier, hash); - - switch (loaded_result.unwrap(vm.jsc, .leave_unhandled)) { - .rejected => |result| { - vm.unhandledRejection(vm.global, result, loaded_result.asValue()); - vm.disableMacroMode(); - return error.MacroLoadError; - }, - else => {}, - } - - return Macro{ - .vm = vm, - .resolver = resolver, - }; - } - - pub const Runner = struct { - const VisitMap = std.AutoHashMapUnmanaged(JSC.JSValue, Expr); - - threadlocal var args_buf: [3]js.JSObjectRef = undefined; - threadlocal var exception_holder: JSC.ZigException.Holder = undefined; - pub const MacroError = error{ MacroFailed, OutOfMemory } || ToJSError || bun.JSError; - - pub const Run = struct { - caller: Expr, - function_name: string, - macro: *const Macro, - global: *JSC.JSGlobalObject, - allocator: std.mem.Allocator, - id: i32, - log: *logger.Log, - source: *const logger.Source, - visited: VisitMap = VisitMap{}, - is_top_level: bool = false, - - pub fn runAsync( - macro: Macro, - log: *logger.Log, - allocator: std.mem.Allocator, - function_name: string, - caller: Expr, - args: []JSC.JSValue, - source: *const logger.Source, - id: i32, - ) MacroError!Expr { - const macro_callback = macro.vm.macros.get(id) orelse return caller; - - const result = js.JSObjectCallAsFunctionReturnValueHoldingAPILock( - macro.vm.global, - macro_callback, - null, - args.len, - @as([*]js.JSObjectRef, @ptrCast(args.ptr)), - ); - - var runner = Run{ - .caller = caller, - .function_name = function_name, - .macro = ¯o, - .allocator = allocator, - .global = macro.vm.global, - .id = id, - .log = log, - .source = source, - .visited = VisitMap{}, - }; - - defer runner.visited.deinit(allocator); - - return try runner.run( - result, - ); - } - - pub fn run( - this: *Run, - value: JSC.JSValue, - ) MacroError!Expr { - return switch ((try JSC.ConsoleObject.Formatter.Tag.get(value, this.global)).tag) { - .Error => this.coerce(value, .Error), - .Undefined => this.coerce(value, .Undefined), - .Null => this.coerce(value, .Null), - .Private => this.coerce(value, .Private), - .Boolean => this.coerce(value, .Boolean), - .Array => this.coerce(value, .Array), - .Object => this.coerce(value, .Object), - .toJSON, .JSON => this.coerce(value, .JSON), - .Integer => this.coerce(value, .Integer), - .Double => this.coerce(value, .Double), - .String => this.coerce(value, .String), - .Promise => this.coerce(value, .Promise), - else => brk: { - const name = value.getClassInfoName() orelse "unknown"; - - this.log.addErrorFmt( - this.source, - this.caller.loc, - this.allocator, - "cannot coerce {s} ({s}) to Bun's AST. Please return a simpler type", - .{ name, @tagName(value.jsType()) }, - ) catch unreachable; - break :brk error.MacroFailed; - }, - }; - } - - pub fn coerce( - this: *Run, - value: JSC.JSValue, - comptime tag: JSC.ConsoleObject.Formatter.Tag, - ) MacroError!Expr { - switch (comptime tag) { - .Error => { - _ = this.macro.vm.uncaughtException(this.global, value, false); - return this.caller; - }, - .Undefined => if (this.is_top_level) - return this.caller - else - return Expr.init(E.Undefined, E.Undefined{}, this.caller.loc), - .Null => return Expr.init(E.Null, E.Null{}, this.caller.loc), - .Private => { - this.is_top_level = false; - const _entry = this.visited.getOrPut(this.allocator, value) catch unreachable; - if (_entry.found_existing) { - return _entry.value_ptr.*; - } - - var blob_: ?JSC.WebCore.Blob = null; - const mime_type: ?MimeType = null; - - if (value.jsType() == .DOMWrapper) { - if (value.as(JSC.WebCore.Response)) |resp| { - return this.run(try resp.getBlobWithoutCallFrame(this.global)); - } else if (value.as(JSC.WebCore.Request)) |resp| { - return this.run(try resp.getBlobWithoutCallFrame(this.global)); - } else if (value.as(JSC.WebCore.Blob)) |resp| { - blob_ = resp.*; - blob_.?.allocator = null; - } else if (value.as(bun.api.ResolveMessage) != null or value.as(bun.api.BuildMessage) != null) { - _ = this.macro.vm.uncaughtException(this.global, value, false); - return error.MacroFailed; - } - } - - if (blob_) |*blob| { - const out_expr = Expr.fromBlob( - blob, - this.allocator, - mime_type, - this.log, - this.caller.loc, - ) catch { - blob.deinit(); - return error.MacroFailed; - }; - if (out_expr.data == .e_string) { - blob.deinit(); - } - - return out_expr; - } - - return Expr.init(E.String, E.String.empty, this.caller.loc); - }, - - .Boolean => { - return Expr{ .data = .{ .e_boolean = .{ .value = value.toBoolean() } }, .loc = this.caller.loc }; - }, - JSC.ConsoleObject.Formatter.Tag.Array => { - this.is_top_level = false; - - const _entry = this.visited.getOrPut(this.allocator, value) catch unreachable; - if (_entry.found_existing) { - switch (_entry.value_ptr.*.data) { - .e_object, .e_array => { - this.log.addErrorFmt(this.source, this.caller.loc, this.allocator, "converting circular structure to Bun AST is not implemented yet", .{}) catch unreachable; - return error.MacroFailed; - }, - else => {}, - } - return _entry.value_ptr.*; - } - - var iter = try JSC.JSArrayIterator.init(value, this.global); - if (iter.len == 0) { - const result = Expr.init( - E.Array, - E.Array{ - .items = ExprNodeList.init(&[_]Expr{}), - .was_originally_macro = true, - }, - this.caller.loc, - ); - _entry.value_ptr.* = result; - return result; - } - var array = this.allocator.alloc(Expr, iter.len) catch unreachable; - var out = Expr.init( - E.Array, - E.Array{ - .items = ExprNodeList.init(array[0..0]), - .was_originally_macro = true, - }, - this.caller.loc, - ); - _entry.value_ptr.* = out; - - errdefer this.allocator.free(array); - var i: usize = 0; - while (try iter.next()) |item| { - array[i] = try this.run(item); - if (array[i].isMissing()) - continue; - i += 1; - } - out.data.e_array.items = ExprNodeList.init(array); - _entry.value_ptr.* = out; - return out; - }, - // TODO: optimize this - JSC.ConsoleObject.Formatter.Tag.Object => { - this.is_top_level = false; - const _entry = this.visited.getOrPut(this.allocator, value) catch unreachable; - if (_entry.found_existing) { - switch (_entry.value_ptr.*.data) { - .e_object, .e_array => { - this.log.addErrorFmt(this.source, this.caller.loc, this.allocator, "converting circular structure to Bun AST is not implemented yet", .{}) catch unreachable; - return error.MacroFailed; - }, - else => {}, - } - return _entry.value_ptr.*; - } - // SAFETY: tag ensures `value` is an object. - const obj = value.getObject() orelse unreachable; - var object_iter = try JSC.JSPropertyIterator(.{ - .skip_empty_name = false, - .include_value = true, - }).init(this.global, obj); - defer object_iter.deinit(); - var properties = this.allocator.alloc(G.Property, object_iter.len) catch unreachable; - errdefer this.allocator.free(properties); - var out = Expr.init( - E.Object, - E.Object{ - .properties = BabyList(G.Property).init(properties), - .was_originally_macro = true, - }, - this.caller.loc, - ); - _entry.value_ptr.* = out; - - while (try object_iter.next()) |prop| { - properties[object_iter.i] = G.Property{ - .key = Expr.init(E.String, E.String.init(prop.toOwnedSlice(this.allocator) catch unreachable), this.caller.loc), - .value = try this.run(object_iter.value), - }; - } - out.data.e_object.properties = BabyList(G.Property).init(properties[0..object_iter.i]); - _entry.value_ptr.* = out; - return out; - }, - - .JSON => { - this.is_top_level = false; - // 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(this.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 = @as(f64, @floatFromInt(value.toInt32())) }, this.caller.loc); - }, - .Double => { - return Expr.init(E.Number, E.Number{ .value = value.asNumber() }, this.caller.loc); - }, - .String => { - var bun_str = try value.toBunString(this.global); - defer bun_str.deref(); - - // encode into utf16 so the printer escapes the string correctly - var utf16_bytes = this.allocator.alloc(u16, bun_str.length()) catch unreachable; - const out_slice = utf16_bytes[0 .. (bun_str.encodeInto(std.mem.sliceAsBytes(utf16_bytes), .utf16le) catch 0) / 2]; - return Expr.init(E.String, E.String.init(out_slice), this.caller.loc); - }, - .Promise => { - const _entry = this.visited.getOrPut(this.allocator, value) catch unreachable; - if (_entry.found_existing) { - return _entry.value_ptr.*; - } - - const promise = value.asAnyPromise() orelse @panic("Unexpected promise type"); - - this.macro.vm.waitForPromise(promise); - - const promise_result = promise.result(this.macro.vm.jsc); - const rejected = promise.status(this.macro.vm.jsc) == .rejected; - - if (promise_result.isUndefined() and this.is_top_level) { - this.is_top_level = false; - return this.caller; - } - - if (rejected or promise_result.isError() or promise_result.isAggregateError(this.global) or promise_result.isException(this.global.vm())) { - this.macro.vm.unhandledRejection(this.global, promise_result, promise.asValue()); - return error.MacroFailed; - } - this.is_top_level = false; - const result = try this.run(promise_result); - - _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 simpler type", - .{@tagName(value.jsType())}, - ) catch unreachable; - return error.MacroFailed; - } - }; - - pub fn run( - macro: Macro, - log: *logger.Log, - allocator: std.mem.Allocator, - function_name: string, - caller: Expr, - source: *const logger.Source, - id: i32, - javascript_object: JSC.JSValue, - ) MacroError!Expr { - if (comptime Environment.isDebug) Output.prettyln("[macro] call {s}", .{function_name}); - - exception_holder = JSC.ZigException.Holder.init(); - var js_args: []JSC.JSValue = &.{}; - var js_processed_args_len: usize = 0; - defer { - for (js_args[0..js_processed_args_len -| @as(usize, @intFromBool(javascript_object != .zero))]) |arg| { - arg.unprotect(); - } - - allocator.free(js_args); - } - - const globalObject = JSC.VirtualMachine.get().global; - - switch (caller.data) { - .e_call => |call| { - const call_args: []Expr = call.args.slice(); - js_args = try allocator.alloc(JSC.JSValue, call_args.len + @as(usize, @intFromBool(javascript_object != .zero))); - js_processed_args_len = js_args.len; - - for (0.., call_args, js_args[0..call_args.len]) |i, in, *out| { - const value = in.toJS( - allocator, - globalObject, - ) catch |e| { - // Keeping a separate variable instead of modifying js_args.len - // due to allocator.free call in defer - js_processed_args_len = i; - return e; - }; - value.protect(); - out.* = value; - } - }, - .e_template => { - @panic("TODO: support template literals in macros"); - }, - else => { - @panic("Unexpected caller type"); - }, - } - - if (javascript_object != .zero) { - if (js_args.len == 0) { - js_args = try allocator.alloc(JSC.JSValue, 1); - } - - js_args[js_args.len - 1] = javascript_object; - } - - const CallFunction = @TypeOf(Run.runAsync); - const CallArgs = std.meta.ArgsTuple(CallFunction); - const CallData = struct { - threadlocal var call_args: CallArgs = undefined; - threadlocal var result: MacroError!Expr = undefined; - pub fn callWrapper(args: CallArgs) MacroError!Expr { - JSC.markBinding(@src()); - call_args = args; - Bun__startMacro(&call, JSC.VirtualMachine.get().global); - return result; - } - - pub fn call() callconv(.C) void { - const call_args_copy = call_args; - const local_result = @call(.auto, Run.runAsync, call_args_copy); - result = local_result; - } - }; - - // TODO: can change back to `return CallData.callWrapper(.{` - // when https://github.com/ziglang/zig/issues/16242 is fixed - return CallData.callWrapper(CallArgs{ - macro, - log, - allocator, - function_name, - caller, - js_args, - source, - id, - }); - } - - extern "c" fn Bun__startMacro(function: *const anyopaque, *anyopaque) void; - }; -}; - -pub const ASTMemoryAllocator = struct { - const SFA = std.heap.StackFallbackAllocator(@min(8192, std.heap.page_size_min)); - - stack_allocator: SFA = undefined, - bump_allocator: std.mem.Allocator = undefined, - allocator: std.mem.Allocator, - previous: ?*ASTMemoryAllocator = null, - - pub fn enter(this: *ASTMemoryAllocator, allocator: std.mem.Allocator) ASTMemoryAllocator.Scope { - this.allocator = allocator; - this.stack_allocator = SFA{ - .buffer = undefined, - .fallback_allocator = allocator, - .fixed_buffer_allocator = undefined, - }; - this.bump_allocator = this.stack_allocator.get(); - this.previous = null; - var ast_scope = ASTMemoryAllocator.Scope{ - .current = this, - .previous = Stmt.Data.Store.memory_allocator, - }; - ast_scope.enter(); - return ast_scope; - } - pub const Scope = struct { - current: ?*ASTMemoryAllocator = null, - previous: ?*ASTMemoryAllocator = null, - - pub fn enter(this: *@This()) void { - bun.debugAssert(Expr.Data.Store.memory_allocator == Stmt.Data.Store.memory_allocator); - - this.previous = Expr.Data.Store.memory_allocator; - - const current = this.current; - - Expr.Data.Store.memory_allocator = current; - Stmt.Data.Store.memory_allocator = current; - - if (current == null) { - Stmt.Data.Store.begin(); - Expr.Data.Store.begin(); - } - } - - pub fn exit(this: *const @This()) void { - Expr.Data.Store.memory_allocator = this.previous; - Stmt.Data.Store.memory_allocator = this.previous; - } - }; - - pub fn reset(this: *ASTMemoryAllocator) void { - this.stack_allocator = SFA{ - .buffer = undefined, - .fallback_allocator = this.allocator, - .fixed_buffer_allocator = undefined, - }; - this.bump_allocator = this.stack_allocator.get(); - } - - pub fn push(this: *ASTMemoryAllocator) void { - Stmt.Data.Store.memory_allocator = this; - Expr.Data.Store.memory_allocator = this; - } - - pub fn pop(this: *ASTMemoryAllocator) void { - const prev = this.previous; - bun.assert(prev != this); - Stmt.Data.Store.memory_allocator = prev; - Expr.Data.Store.memory_allocator = prev; - this.previous = null; - } - - pub fn append(this: ASTMemoryAllocator, comptime ValueType: type, value: anytype) *ValueType { - const ptr = this.bump_allocator.create(ValueType) catch unreachable; - ptr.* = value; - return ptr; - } - - /// Initialize ASTMemoryAllocator as `undefined`, and call this. - pub fn initWithoutStack(this: *ASTMemoryAllocator, arena: std.mem.Allocator) void { - this.stack_allocator = SFA{ - .buffer = undefined, - .fallback_allocator = arena, - .fixed_buffer_allocator = .init(&.{}), - }; - this.bump_allocator = this.stack_allocator.get(); - } -}; - -pub const UseDirective = enum(u2) { - // TODO: Remove this, and provide `UseDirective.Optional` instead - none, - /// "use client" - client, - /// "use server" - server, - - pub const Boundering = enum(u2) { - client = @intFromEnum(UseDirective.client), - server = @intFromEnum(UseDirective.server), - }; - - pub const Flags = struct { - has_any_client: bool = false, - }; - - pub fn isBoundary(this: UseDirective, other: UseDirective) bool { - if (this == other or other == .none) - return false; - - return true; - } - - pub fn boundering(this: UseDirective, other: UseDirective) ?Boundering { - if (this == other or other == .none) - return null; - return @enumFromInt(@intFromEnum(other)); - } - - pub fn parse(contents: []const u8) ?UseDirective { - const truncated = std.mem.trimLeft(u8, contents, " \t\n\r;"); - - if (truncated.len < "'use client';".len) - return .none; - - const directive_string = truncated[0.."'use client';".len].*; - - const first_quote = directive_string[0]; - const last_quote = directive_string[directive_string.len - 2]; - if (first_quote != last_quote or (first_quote != '"' and first_quote != '\'' and first_quote != '`')) - return .none; - - const unquoted = directive_string[1 .. directive_string.len - 2]; - - if (strings.eqlComptime(unquoted, "use client")) { - return .client; - } - - if (strings.eqlComptime(unquoted, "use server")) { - return .server; - } - - return null; - } -}; - -/// Represents a boundary between client and server code. Every boundary -/// gets bundled twice, once for the desired target, and once to generate -/// a module of "references". Specifically, the generated file takes the -/// canonical Ast as input to derive a wrapper. See `Framework.ServerComponents` -/// for more details about this generated file. -/// -/// This is sometimes abbreviated as SCB -pub const ServerComponentBoundary = struct { - use_directive: UseDirective, - - /// The index of the original file. - source_index: Index.Int, - - /// Index to the file imported on the opposite platform, which is - /// generated by the bundler. For client components, this is the - /// server's code. For server actions, this is the client's code. - reference_source_index: Index.Int, - - /// When `bake.Framework.ServerComponents.separate_ssr_graph` is enabled this - /// points to the separated module. When the SSR graph is not separate, this is - /// equal to `reference_source_index` - // - // TODO: Is this used for server actions. - ssr_source_index: Index.Int, - - /// The requirements for this data structure is to have reasonable lookup - /// speed, but also being able to pull a `[]const Index.Int` of all - /// boundaries for iteration. - pub const List = struct { - list: std.MultiArrayList(ServerComponentBoundary) = .{}, - /// Used to facilitate fast lookups into `items` by `.source_index` - map: Map = .{}, - - const Map = std.ArrayHashMapUnmanaged(void, void, struct {}, true); - - /// Can only be called on the bundler thread. - pub fn put( - m: *List, - allocator: std.mem.Allocator, - source_index: Index.Int, - use_directive: UseDirective, - reference_source_index: Index.Int, - ssr_source_index: Index.Int, - ) !void { - try m.list.append(allocator, .{ - .source_index = source_index, - .use_directive = use_directive, - .reference_source_index = reference_source_index, - .ssr_source_index = ssr_source_index, - }); - const gop = try m.map.getOrPutAdapted( - allocator, - source_index, - Adapter{ .list = m.list.slice() }, - ); - bun.assert(!gop.found_existing); - } - - /// Can only be called on the bundler thread. - pub fn getIndex(l: *const List, real_source_index: Index.Int) ?usize { - return l.map.getIndexAdapted( - real_source_index, - Adapter{ .list = l.list.slice() }, - ); - } - - /// Use this to improve speed of accessing fields at the cost of - /// storing more pointers. Invalidated when input is mutated. - pub fn slice(l: List) Slice { - return .{ .list = l.list.slice(), .map = l.map }; - } - - pub const Slice = struct { - list: std.MultiArrayList(ServerComponentBoundary).Slice, - map: Map, - - pub fn getIndex(l: *const Slice, real_source_index: Index.Int) ?usize { - return l.map.getIndexAdapted( - real_source_index, - Adapter{ .list = l.list }, - ) orelse return null; - } - - pub fn getReferenceSourceIndex(l: *const Slice, real_source_index: Index.Int) ?u32 { - const i = l.map.getIndexAdapted( - real_source_index, - Adapter{ .list = l.list }, - ) orelse return null; - bun.unsafeAssert(l.list.capacity > 0); // optimize MultiArrayList.Slice.items - return l.list.items(.reference_source_index)[i]; - } - - pub fn bitSet(scbs: Slice, alloc: std.mem.Allocator, input_file_count: usize) !bun.bit_set.DynamicBitSetUnmanaged { - var scb_bitset = try bun.bit_set.DynamicBitSetUnmanaged.initEmpty(alloc, input_file_count); - for (scbs.list.items(.source_index)) |source_index| { - scb_bitset.set(source_index); - } - return scb_bitset; - } - }; - - pub const Adapter = struct { - list: std.MultiArrayList(ServerComponentBoundary).Slice, - - pub fn hash(_: Adapter, key: Index.Int) u32 { - return std.hash.uint32(key); - } - - pub fn eql(adapt: Adapter, a: Index.Int, _: void, b_index: usize) bool { - bun.unsafeAssert(adapt.list.capacity > 0); // optimize MultiArrayList.Slice.items - return a == adapt.list.items(.source_index)[b_index]; - } - }; - }; -}; - -extern fn JSC__jsToNumber(latin1_ptr: [*]const u8, len: usize) f64; - -fn stringToEquivalentNumberValue(str: []const u8) f64 { - // +"" -> 0 - if (str.len == 0) return 0; - if (!bun.strings.isAllASCII(str)) - return std.math.nan(f64); - return JSC__jsToNumber(str.ptr, str.len); -} - -// test "Binding.init" { -// var binding = Binding.alloc( -// std.heap.page_allocator, -// B.Identifier{ .ref = Ref{ .source_index = 0, .innerIndex() = 10 } }, -// logger.Loc{ .start = 1 }, -// ); -// std.testing.expect(binding.loc.start == 1); -// std.testing.expect(@as(Binding.Tag, binding.data) == Binding.Tag.b_identifier); - -// printmem("-------Binding: {d} bits\n", .{@bitSizeOf(Binding)}); -// printmem("B.Identifier: {d} bits\n", .{@bitSizeOf(B.Identifier)}); -// printmem("B.Array: {d} bits\n", .{@bitSizeOf(B.Array)}); -// printmem("B.Property: {d} bits\n", .{@bitSizeOf(B.Property)}); -// printmem("B.Object: {d} bits\n", .{@bitSizeOf(B.Object)}); -// printmem("B.Missing: {d} bits\n", .{@bitSizeOf(B.Missing)}); -// printmem("-------Binding: {d} bits\n", .{@bitSizeOf(Binding)}); -// } - -// test "Stmt.init" { -// var stmt = Stmt.alloc( -// std.heap.page_allocator, -// S.Continue{}, -// logger.Loc{ .start = 1 }, -// ); -// std.testing.expect(stmt.loc.start == 1); -// std.testing.expect(@as(Stmt.Tag, stmt.data) == Stmt.Tag.s_continue); - -// printmem("-----Stmt {d} bits\n", .{@bitSizeOf(Stmt)}); -// printmem("StmtNodeList: {d} bits\n", .{@bitSizeOf(StmtNodeList)}); -// printmem("StmtOrExpr: {d} bits\n", .{@bitSizeOf(StmtOrExpr)}); -// printmem("S.Block {d} bits\n", .{@bitSizeOf(S.Block)}); -// printmem("S.Comment {d} bits\n", .{@bitSizeOf(S.Comment)}); -// printmem("S.Directive {d} bits\n", .{@bitSizeOf(S.Directive)}); -// printmem("S.ExportClause {d} bits\n", .{@bitSizeOf(S.ExportClause)}); -// printmem("S.Empty {d} bits\n", .{@bitSizeOf(S.Empty)}); -// printmem("S.TypeScript {d} bits\n", .{@bitSizeOf(S.TypeScript)}); -// printmem("S.Debugger {d} bits\n", .{@bitSizeOf(S.Debugger)}); -// printmem("S.ExportFrom {d} bits\n", .{@bitSizeOf(S.ExportFrom)}); -// printmem("S.ExportDefault {d} bits\n", .{@bitSizeOf(S.ExportDefault)}); -// printmem("S.Enum {d} bits\n", .{@bitSizeOf(S.Enum)}); -// printmem("S.Namespace {d} bits\n", .{@bitSizeOf(S.Namespace)}); -// printmem("S.Function {d} bits\n", .{@bitSizeOf(S.Function)}); -// printmem("S.Class {d} bits\n", .{@bitSizeOf(S.Class)}); -// printmem("S.If {d} bits\n", .{@bitSizeOf(S.If)}); -// printmem("S.For {d} bits\n", .{@bitSizeOf(S.For)}); -// printmem("S.ForIn {d} bits\n", .{@bitSizeOf(S.ForIn)}); -// printmem("S.ForOf {d} bits\n", .{@bitSizeOf(S.ForOf)}); -// printmem("S.DoWhile {d} bits\n", .{@bitSizeOf(S.DoWhile)}); -// printmem("S.While {d} bits\n", .{@bitSizeOf(S.While)}); -// printmem("S.With {d} bits\n", .{@bitSizeOf(S.With)}); -// printmem("S.Try {d} bits\n", .{@bitSizeOf(S.Try)}); -// printmem("S.Switch {d} bits\n", .{@bitSizeOf(S.Switch)}); -// printmem("S.Import {d} bits\n", .{@bitSizeOf(S.Import)}); -// printmem("S.Return {d} bits\n", .{@bitSizeOf(S.Return)}); -// printmem("S.Throw {d} bits\n", .{@bitSizeOf(S.Throw)}); -// printmem("S.Local {d} bits\n", .{@bitSizeOf(S.Local)}); -// printmem("S.Break {d} bits\n", .{@bitSizeOf(S.Break)}); -// printmem("S.Continue {d} bits\n", .{@bitSizeOf(S.Continue)}); -// printmem("-----Stmt {d} bits\n", .{@bitSizeOf(Stmt)}); -// } - -// test "Expr.init" { -// var allocator = std.heap.page_allocator; -// const ident = Expr.init(E.Identifier, E.Identifier{}, logger.Loc{ .start = 100 }); -// var list = [_]Expr{ident}; -// var expr = Expr.init( -// E.Array, -// E.Array{ .items = list[0..] }, -// logger.Loc{ .start = 1 }, -// ); -// try std.testing.expect(expr.loc.start == 1); -// try std.testing.expect(@as(Expr.Tag, expr.data) == Expr.Tag.e_array); -// try std.testing.expect(expr.data.e_array.items[0].loc.start == 100); - -// printmem("--Ref {d} bits\n", .{@bitSizeOf(Ref)}); -// printmem("--LocRef {d} bits\n", .{@bitSizeOf(LocRef)}); -// printmem("--logger.Loc {d} bits\n", .{@bitSizeOf(logger.Loc)}); -// printmem("--logger.Range {d} bits\n", .{@bitSizeOf(logger.Range)}); -// printmem("----------Expr: {d} bits\n", .{@bitSizeOf(Expr)}); -// printmem("ExprNodeList: {d} bits\n", .{@bitSizeOf(ExprNodeList)}); -// printmem("E.Array: {d} bits\n", .{@bitSizeOf(E.Array)}); - -// printmem("E.Unary: {d} bits\n", .{@bitSizeOf(E.Unary)}); -// printmem("E.Binary: {d} bits\n", .{@bitSizeOf(E.Binary)}); -// printmem("E.Boolean: {d} bits\n", .{@bitSizeOf(E.Boolean)}); -// printmem("E.Super: {d} bits\n", .{@bitSizeOf(E.Super)}); -// printmem("E.Null: {d} bits\n", .{@bitSizeOf(E.Null)}); -// printmem("E.Undefined: {d} bits\n", .{@bitSizeOf(E.Undefined)}); -// printmem("E.New: {d} bits\n", .{@bitSizeOf(E.New)}); -// printmem("E.NewTarget: {d} bits\n", .{@bitSizeOf(E.NewTarget)}); -// printmem("E.Function: {d} bits\n", .{@bitSizeOf(E.Function)}); -// printmem("E.ImportMeta: {d} bits\n", .{@bitSizeOf(E.ImportMeta)}); -// printmem("E.Call: {d} bits\n", .{@bitSizeOf(E.Call)}); -// printmem("E.Dot: {d} bits\n", .{@bitSizeOf(E.Dot)}); -// printmem("E.Index: {d} bits\n", .{@bitSizeOf(E.Index)}); -// printmem("E.Arrow: {d} bits\n", .{@bitSizeOf(E.Arrow)}); -// printmem("E.Identifier: {d} bits\n", .{@bitSizeOf(E.Identifier)}); -// printmem("E.ImportIdentifier: {d} bits\n", .{@bitSizeOf(E.ImportIdentifier)}); -// printmem("E.PrivateIdentifier: {d} bits\n", .{@bitSizeOf(E.PrivateIdentifier)}); -// printmem("E.JSXElement: {d} bits\n", .{@bitSizeOf(E.JSXElement)}); -// printmem("E.Missing: {d} bits\n", .{@bitSizeOf(E.Missing)}); -// printmem("E.Number: {d} bits\n", .{@bitSizeOf(E.Number)}); -// printmem("E.BigInt: {d} bits\n", .{@bitSizeOf(E.BigInt)}); -// printmem("E.Object: {d} bits\n", .{@bitSizeOf(E.Object)}); -// printmem("E.Spread: {d} bits\n", .{@bitSizeOf(E.Spread)}); -// printmem("E.String: {d} bits\n", .{@bitSizeOf(E.String)}); -// printmem("E.TemplatePart: {d} bits\n", .{@bitSizeOf(E.TemplatePart)}); -// printmem("E.Template: {d} bits\n", .{@bitSizeOf(E.Template)}); -// printmem("E.RegExp: {d} bits\n", .{@bitSizeOf(E.RegExp)}); -// printmem("E.Await: {d} bits\n", .{@bitSizeOf(E.Await)}); -// printmem("E.Yield: {d} bits\n", .{@bitSizeOf(E.Yield)}); -// printmem("E.If: {d} bits\n", .{@bitSizeOf(E.If)}); -// printmem("E.RequireResolveString: {d} bits\n", .{@bitSizeOf(E.RequireResolveString)}); -// printmem("E.Import: {d} bits\n", .{@bitSizeOf(E.Import)}); -// printmem("----------Expr: {d} bits\n", .{@bitSizeOf(Expr)}); -// } - -// -- ESBuild bit sizes -// EArray | 256 -// EArrow | 512 -// EAwait | 192 -// EBinary | 448 -// ECall | 448 -// EDot | 384 -// EIdentifier | 96 -// EIf | 576 -// EImport | 448 -// EImportIdentifier | 96 -// EIndex | 448 -// EJSXElement | 448 -// ENew | 448 -// EnumValue | 384 -// EObject | 256 -// EPrivateIdentifier | 64 -// ERequire | 32 -// ERequireResolve | 32 -// EString | 256 -// ETemplate | 640 -// EUnary | 256 -// Expr | 192 -// ExprOrStmt | 128 -// EYield | 128 -// Finally | 256 -// Fn | 704 -// FnBody | 256 -// LocRef | 96 -// NamedExport | 96 -// NamedImport | 512 -// NameMinifier | 256 -// NamespaceAlias | 192 -// opTableEntry | 256 -// Part | 1088 -// Property | 640 -// PropertyBinding | 512 -// Ref | 64 -// SBlock | 192 -// SBreak | 64 -// SClass | 704 -// SComment | 128 -// SContinue | 64 -// Scope | 704 -// ScopeMember | 96 -// SDirective | 256 -// SDoWhile | 384 -// SEnum | 448 -// SExportClause | 256 -// SExportDefault | 256 -// SExportEquals | 192 -// SExportFrom | 320 -// SExportStar | 192 -// SExpr | 256 -// SFor | 384 -// SForIn | 576 -// SForOf | 640 -// SFunction | 768 -// SIf | 448 -// SImport | 320 -// SLabel | 320 -// SLazyExport | 192 -// SLocal | 256 -// SNamespace | 448 -// Span | 192 -// SReturn | 64 -// SSwitch | 448 -// SThrow | 192 -// Stmt | 192 -// STry | 384 -// -- ESBuild bit sizes - -const ToJSError = error{ +pub const ToJSError = error{ @"Cannot convert argument type to JS", @"Cannot convert identifier to JS. Try a statically-known value", MacroError, @@ -9130,8 +597,6 @@ const ToJSError = error{ JSError, }; -const writeAnyToHasher = bun.writeAnyToHasher; - /// Say you need to allocate a bunch of tiny arrays /// You could just do separate allocations for each, but that is slow /// With std.ArrayList, pointers invalidate on resize and that means it will crash. @@ -9169,3 +634,48 @@ pub fn NewBatcher(comptime Type: type) type { } }; } + +// @sortImports + +pub const ASTMemoryAllocator = @import("ast/ASTMemoryAllocator.zig"); +pub const Ast = @import("ast/Ast.zig"); +pub const Binding = @import("ast/Binding.zig"); +pub const BindingNodeIndex = Binding; +pub const BundledAst = @import("ast/BundledAst.zig"); +pub const E = @import("ast/E.zig"); +pub const Expr = @import("ast/Expr.zig"); +pub const ExprNodeIndex = Expr; +pub const G = @import("ast/G.zig"); +pub const Macro = @import("ast/Macro.zig"); +pub const Op = @import("ast/Op.zig"); +pub const S = @import("ast/S.zig"); +pub const Scope = @import("ast/Scope.zig"); +pub const ServerComponentBoundary = @import("ast/ServerComponentBoundary.zig"); +pub const Stmt = @import("ast/Stmt.zig"); +pub const StmtNodeIndex = Stmt; +pub const Symbol = @import("ast/Symbol.zig"); +const std = @import("std"); +pub const B = @import("ast/B.zig").B; +pub const NewStore = @import("ast/NewStore.zig").NewStore; +const TypeScript = @import("./js_parser.zig").TypeScript; +pub const UseDirective = @import("ast/UseDirective.zig").UseDirective; + +pub const CharFreq = @import("ast/CharFreq.zig"); +const char_freq_count = CharFreq.char_freq_count; + +pub const TS = @import("ast/TS.zig"); +pub const TSNamespaceMember = TS.TSNamespaceMember; +pub const TSNamespaceMemberMap = TS.TSNamespaceMemberMap; +pub const TSNamespaceScope = TS.TSNamespaceScope; + +pub const Index = @import("ast/base.zig").Index; +pub const Ref = @import("ast/base.zig").Ref; +pub const RefHashCtx = @import("ast/base.zig").RefHashCtx; + +const bun = @import("bun"); +pub const BabyList = bun.BabyList; +const Environment = bun.Environment; +const Output = bun.Output; +const logger = bun.logger; +const string = bun.string; +const strings = bun.strings;