const std = @import("std"); const ast = @import("ast.zig"); const parser = @import("parser.zig"); const imports = @import("imports.zig"); const sema = @import("sema.zig"); const errors = @import("errors.zig"); const c_import = @import("c_import.zig"); const ir = @import("ir/ir.zig"); const target_mod = @import("target.zig"); const Node = ast.Node; pub const TargetConfig = target_mod.TargetConfig; pub const Compilation = struct { allocator: std.mem.Allocator, io: std.Io, file_path: []const u8, source: [:0]const u8, diagnostics: errors.DiagnosticList, target_config: TargetConfig, stdlib_paths: []const []const u8 = &.{}, // Pipeline results root: ?*Node = null, resolved_root: ?*Node = null, import_sources: std.StringHashMap([:0]const u8), module_scopes: std.StringHashMap(std.StringHashMap(void)), import_graph: std.StringHashMap(std.StringHashMap(void)), sema_result: ?sema.SemaResult = null, ir_emitter: ?ir.LLVMEmitter = null, pub fn init(allocator: std.mem.Allocator, io: std.Io, file_path: []const u8, source: [:0]const u8, target_config: TargetConfig, stdlib_paths: []const []const u8) Compilation { return .{ .allocator = allocator, .io = io, .file_path = file_path, .source = source, .diagnostics = errors.DiagnosticList.init(allocator, source, file_path), .import_sources = std.StringHashMap([:0]const u8).init(allocator), .module_scopes = std.StringHashMap(std.StringHashMap(void)).init(allocator), .import_graph = std.StringHashMap(std.StringHashMap(void)).init(allocator), .target_config = target_config, .stdlib_paths = stdlib_paths, }; } pub fn deinit(self: *Compilation) void { if (self.ir_emitter) |*e| e.deinit(); self.diagnostics.deinit(); } pub fn parse(self: *Compilation) !void { var p = parser.Parser.init(self.allocator, self.source); p.diagnostics = &self.diagnostics; self.root = p.parse() catch return error.CompileError; } pub fn resolveImports(self: *Compilation) !void { const root = self.root orelse return error.CompileError; var chain = std.StringHashMap(void).init(self.allocator); var cache = imports.ModuleCache.init(self.allocator); const base_dir = imports.dirName(self.file_path); const mod = imports.resolveImports( self.allocator, self.io, root, base_dir, self.file_path, &chain, &cache, &self.import_sources, &self.diagnostics, self.stdlib_paths, &self.import_graph, ) catch return error.CompileError; // Preserve per-module visibility scopes for C import access checking self.module_scopes.put(self.file_path, mod.scope) catch {}; var cache_it = cache.iterator(); while (cache_it.next()) |entry| { self.module_scopes.put(entry.key_ptr.*, entry.value_ptr.scope) catch {}; } // Store main file source in import_sources so error reporting can find it self.import_sources.put(self.file_path, self.source) catch {}; // Wire import_sources to diagnostics for file-aware error rendering self.diagnostics.import_sources = &self.import_sources; // Build a root node from the resolved module's decls const new_root = try self.allocator.create(Node); new_root.* = .{ .span = root.span, .data = .{ .root = .{ .decls = mod.decls } }, }; self.resolved_root = new_root; } pub fn analyze(self: *Compilation) !void { const root = self.resolved_root orelse self.root orelse return error.CompileError; var analyzer = sema.Analyzer.init(self.allocator); self.sema_result = analyzer.analyze(root) catch return error.CompileError; // Merge sema diagnostics into our list if (self.sema_result) |sr| { for (sr.diagnostics) |d| { self.diagnostics.add(d.level, d.message, d.span); } } } /// Generate code via the IR pipeline: lower AST → IR → LLVM. pub fn generateCode(self: *Compilation) !void { // Heap-allocate the IR module so its address is stable during emit const ir_mod_ptr = try self.allocator.create(ir.Module); ir_mod_ptr.* = try self.lowerToIR(); var emitter = ir.LLVMEmitter.init(self.allocator, ir_mod_ptr, "sx_module", self.target_config); emitter.emit(); // IR module is no longer needed after LLVM IR has been generated ir_mod_ptr.deinit(); self.allocator.destroy(ir_mod_ptr); self.ir_emitter = emitter; } /// Get link flags accumulated from #run build blocks. pub fn getBuildLinkFlags(self: *Compilation) []const []const u8 { if (self.ir_emitter) |*e| return e.build_config.link_flags.items; return &.{}; } /// Get frameworks accumulated from #run build blocks (BuildOptions.add_framework). pub fn getBuildFrameworks(self: *Compilation) []const []const u8 { if (self.ir_emitter) |*e| return e.build_config.frameworks.items; return &.{}; } /// Get output path set from #run build blocks, if any. pub fn getBuildOutputPath(self: *Compilation) ?[]const u8 { if (self.ir_emitter) |*e| return e.build_config.output_path; return null; } /// Get custom WASM shell template path set from #run build blocks, if any. pub fn getBuildWasmShell(self: *Compilation) ?[]const u8 { if (self.ir_emitter) |*e| return e.build_config.wasm_shell_path; return null; } /// Collect C import source info from the resolved AST. pub fn collectCImportSources(self: *Compilation) ![]c_import.CImportInfo { const root = self.resolved_root orelse self.root orelse return &.{}; return c_import.collectCImportSources(self.allocator, root); } /// Lower the parsed AST to the sx IR module (shadow pipeline). pub fn lowerToIR(self: *Compilation) !ir.Module { const root = self.resolved_root orelse self.root orelse return ir.Module.init(self.allocator); var module = ir.Module.init(self.allocator); //TODO: find a better place for this if (self.target_config.isWasm32()) { module.types.pointer_size = 4; } var lowering = ir.Lowering.init(&module); lowering.main_file = self.file_path; lowering.resolved_root = root; lowering.target_config = self.target_config; lowering.diagnostics = &self.diagnostics; lowering.module_scopes = &self.module_scopes; lowering.import_graph = &self.import_graph; lowering.lowerRoot(root); if (self.diagnostics.hasErrors()) return error.CompileError; return module; } pub fn renderErrors(self: *const Compilation) void { self.diagnostics.renderDebug(); } };