- examples/modules/ -> library/modules/ (top-level, no more symlink hacks in consumer projects) - compiler discovers stdlib via _NSGetExecutablePath / readlink /proc/self/exe; searches dev layout (../../library), install layout (../library), and alongside-binary fallback - SX_STDLIB_PATH env var overrides for tests / dev convenience - SX_DEBUG_STDLIB env var dumps the discovery results - build.zig installs library/ alongside the binary - Compilation gains stdlib_paths field threaded through resolveImports - 50 tests pass; consumer projects can now build from any cwd
174 lines
6.7 KiB
Zig
174 lines
6.7 KiB
Zig
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)),
|
|
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),
|
|
.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,
|
|
) 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.lowerRoot(root);
|
|
if (self.diagnostics.hasErrors()) return error.CompileError;
|
|
return module;
|
|
}
|
|
|
|
pub fn renderErrors(self: *const Compilation) void {
|
|
self.diagnostics.renderDebug();
|
|
}
|
|
};
|