This commit is contained in:
agra
2026-02-22 17:24:04 +02:00
parent 775dcb44cc
commit d3e574eae5
38 changed files with 16135 additions and 33 deletions

View File

@@ -20,12 +20,26 @@ pub fn build(b: *std.Build) void {
});
mod.addSystemIncludePath(.{ .cwd_relative = include_dir });
mod.addSystemIncludePath(.{ .cwd_relative = "." }); // for clang_shim.h
mod.addLibraryPath(.{ .cwd_relative = lib_dir });
mod.link_libc = true;
mod.addCSourceFile(.{
.file = b.path("llvm_shim.c"),
.flags = &.{b.fmt("-I{s}", .{include_dir})},
});
mod.addCSourceFile(.{
.file = b.path("clang_shim.cpp"),
.flags = &.{
b.fmt("-I{s}", .{include_dir}),
"-std=c++17",
"-fno-rtti",
"-fno-exceptions",
"-D__STDC_CONSTANT_MACROS",
"-D__STDC_FORMAT_MACROS",
"-D__STDC_LIMIT_MACROS",
b.fmt("-DSX_LLVM_PREFIX=\"{s}\"", .{llvm_prefix}),
},
});
const target_os = target.result.os.tag;
@@ -68,12 +82,39 @@ pub fn build(b: *std.Build) void {
}
}
// System libraries LLVM depends on (zlib, zstd, curses, etc.)
// Clang static libraries (for clang_shim: header parsing + C compilation)
const clang_libs_raw = std.mem.trim(u8, b.run(&.{ "sh", "-c", b.fmt("ls {s}/lib/libclang*.a | xargs -n1 basename | sed 's/^lib//;s/\\.a$//'", .{llvm_prefix}) }), " \t\n\r");
var clang_libs_it = std.mem.tokenizeAny(u8, clang_libs_raw, "\n");
while (clang_libs_it.next()) |lib_name| {
const trimmed = std.mem.trim(u8, lib_name, " \t\r");
if (trimmed.len > 0) {
mod.linkSystemLibrary(trimmed, .{ .preferred_link_mode = .static });
}
}
// System libraries LLVM depends on — link statically where possible.
// Add homebrew lib paths for static archives.
if (builtin.os.tag == .macos) {
const homebrew_static_paths = [_][]const u8{
"/opt/homebrew/opt/zlib/lib",
"/opt/homebrew/opt/zstd/lib",
"/opt/homebrew/opt/ncurses/lib",
};
for (&homebrew_static_paths) |p| {
mod.addLibraryPath(.{ .cwd_relative = p });
}
}
const syslibs_raw = std.mem.trim(u8, b.run(&.{ llvm_config, "--system-libs", "--link-static" }), " \t\n\r");
var syslibs_it = std.mem.tokenizeAny(u8, syslibs_raw, " \t\n\r");
while (syslibs_it.next()) |flag| {
if (flag.len > 2 and std.mem.startsWith(u8, flag, "-l")) {
mod.linkSystemLibrary(flag[2..], .{});
const name = flag[2..];
// Skip xml2 — only used by LLVM's Windows manifest parser (not needed)
if (std.mem.eql(u8, name, "xml2")) continue;
// Skip m — part of libSystem on macOS, libc on Linux
if (std.mem.eql(u8, name, "m")) continue;
mod.linkSystemLibrary(name, .{ .preferred_link_mode = .static });
}
}
@@ -96,6 +137,11 @@ pub fn build(b: *std.Build) void {
}
} else {
mod.linkSystemLibrary("LLVM-18", .{});
mod.linkSystemLibrary("clang-cpp", .{});
// clang-cpp is C++ — need libc++ on macOS
if (target_os != .windows and target_os != .linux) {
mod.link_libcpp = true;
}
}
const exe = b.addExecutable(.{

315
clang_shim.cpp Normal file
View File

@@ -0,0 +1,315 @@
#include "clang_shim.h"
// clang C++ API
#include <clang/AST/ASTConsumer.h>
#include <clang/AST/ASTContext.h>
#include <clang/AST/Decl.h>
#include <clang/Basic/DiagnosticOptions.h>
#include <clang/CodeGen/CodeGenAction.h>
#include <clang/Driver/Compilation.h>
#include <clang/Driver/Driver.h>
#include <clang/Frontend/CompilerInstance.h>
#include <clang/Frontend/CompilerInvocation.h>
#include <clang/Frontend/FrontendAction.h>
#include <llvm/IR/LLVMContext.h>
#include <llvm/IR/LegacyPassManager.h>
#include <llvm/IR/Module.h>
#include <llvm/MC/TargetRegistry.h>
#include <llvm/Support/TargetSelect.h>
#include <llvm/Support/raw_ostream.h>
#include <llvm/Target/TargetMachine.h>
#include <llvm/Target/TargetOptions.h>
#include <llvm/TargetParser/Host.h>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
/* ------------------------------------------------------------------ */
/* Header parsing via clang C++ AST */
/* ------------------------------------------------------------------ */
/// AST consumer that collects top-level function declarations.
class FunctionCollector : public clang::ASTConsumer {
public:
std::vector<SxCFunctionInfo> &functions;
clang::SourceManager *SM = nullptr;
clang::FileID mainFileID;
FunctionCollector(std::vector<SxCFunctionInfo> &funcs) : functions(funcs) {}
void Initialize(clang::ASTContext &ctx) override {
SM = &ctx.getSourceManager();
mainFileID = SM->getMainFileID();
}
bool HandleTopLevelDecl(clang::DeclGroupRef DG) override {
for (auto *D : DG) {
auto *FD = llvm::dyn_cast<clang::FunctionDecl>(D);
if (!FD) continue;
// Only include functions from the main file
clang::SourceLocation loc = FD->getLocation();
if (!SM->isInFileID(SM->getExpansionLoc(loc), mainFileID))
continue;
// Skip anonymous/internal functions
std::string name = FD->getNameAsString();
if (name.empty() || name[0] == '_') continue;
// Return type (canonical to resolve typedefs)
std::string ret_type = FD->getReturnType().getCanonicalType().getAsString();
// Parameters
unsigned num_params = FD->getNumParams();
auto *params = num_params > 0
? static_cast<SxCParamInfo *>(calloc(num_params, sizeof(SxCParamInfo)))
: nullptr;
for (unsigned i = 0; i < num_params; i++) {
const clang::ParmVarDecl *P = FD->getParamDecl(i);
std::string pname = P->getNameAsString();
std::string ptype = P->getType().getCanonicalType().getAsString();
params[i].name = strdup(pname.c_str());
params[i].type_spelling = strdup(ptype.c_str());
}
// Source location
clang::PresumedLoc PLoc = SM->getPresumedLoc(loc);
SxCFunctionInfo fi;
fi.name = strdup(name.c_str());
fi.return_type = strdup(ret_type.c_str());
fi.params = params;
fi.num_params = static_cast<int>(num_params);
fi.source_file = PLoc.isValid() ? strdup(PLoc.getFilename()) : nullptr;
fi.source_line = PLoc.isValid() ? PLoc.getLine() : 0;
functions.push_back(fi);
}
return true;
}
};
/// Frontend action that creates a FunctionCollector consumer.
class CollectFunctionsAction : public clang::ASTFrontendAction {
public:
std::vector<SxCFunctionInfo> &functions;
CollectFunctionsAction(std::vector<SxCFunctionInfo> &funcs) : functions(funcs) {}
std::unique_ptr<clang::ASTConsumer>
CreateASTConsumer(clang::CompilerInstance &, llvm::StringRef) override {
return std::make_unique<FunctionCollector>(functions);
}
};
/// Helper: build a CompilerInstance from user args + filename using the Driver.
/// Returns nullptr on failure (sets out_error).
static std::unique_ptr<clang::CompilerInstance>
buildCompilerInstance(const char *filename,
const char **args, int num_args,
const llvm::SmallVectorImpl<const char *> &extra_flags,
char **out_error)
{
auto diagOpts = new clang::DiagnosticOptions();
auto diagIDs = new clang::DiagnosticIDs();
clang::DiagnosticsEngine diags(diagIDs, diagOpts,
new clang::IgnoringDiagConsumer());
clang::driver::Driver drv("clang",
llvm::sys::getDefaultTargetTriple(), diags);
drv.setCheckInputsExist(false);
llvm::SmallVector<const char *, 32> driver_args;
driver_args.push_back("clang");
driver_args.push_back("-c");
driver_args.push_back("-w");
#ifdef SX_LLVM_PREFIX
static std::string resource_dir = std::string(SX_LLVM_PREFIX) + "/lib/clang/18";
driver_args.push_back("-resource-dir");
driver_args.push_back(resource_dir.c_str());
#endif
for (const auto *f : extra_flags)
driver_args.push_back(f);
for (int i = 0; i < num_args; i++)
driver_args.push_back(args[i]);
driver_args.push_back(filename);
std::unique_ptr<clang::driver::Compilation> comp(
drv.BuildCompilation(driver_args));
if (!comp || comp->getJobs().empty()) {
if (out_error) *out_error = strdup("failed to build compilation");
return nullptr;
}
const auto &cmd = llvm::cast<clang::driver::Command>(
*comp->getJobs().begin());
const auto &cc1_args = cmd.getArguments();
auto invocation = std::make_shared<clang::CompilerInvocation>();
bool ok = clang::CompilerInvocation::CreateFromArgs(
*invocation, cc1_args, diags);
if (!ok) {
if (out_error) *out_error = strdup("failed to create compiler invocation");
return nullptr;
}
auto CI = std::make_unique<clang::CompilerInstance>();
CI->setInvocation(std::move(invocation));
CI->createDiagnostics(new clang::IgnoringDiagConsumer());
return CI;
}
extern "C" SxCHeaderInfo *sx_clang_parse_header(
const char *filename,
const char **args, int num_args,
char **out_error)
{
// Parse with -fsyntax-only (no codegen needed)
llvm::SmallVector<const char *, 4> extra;
extra.push_back("-fsyntax-only");
auto CI = buildCompilerInstance(filename, args, num_args, extra, out_error);
if (!CI) return nullptr;
std::vector<SxCFunctionInfo> functions;
CollectFunctionsAction action(functions);
if (!CI->ExecuteAction(action)) {
if (out_error) *out_error = strdup("failed to parse header");
return nullptr;
}
// Convert to C struct
auto *info = static_cast<SxCHeaderInfo *>(malloc(sizeof(SxCHeaderInfo)));
info->num_functions = static_cast<int>(functions.size());
info->functions = static_cast<SxCFunctionInfo *>(
calloc(info->num_functions, sizeof(SxCFunctionInfo)));
for (int i = 0; i < info->num_functions; i++) {
info->functions[i] = functions[i];
}
return info;
}
extern "C" void sx_clang_free_header_info(SxCHeaderInfo *info) {
if (!info) return;
for (int i = 0; i < info->num_functions; i++) {
auto &f = info->functions[i];
free(const_cast<char *>(f.name));
free(const_cast<char *>(f.return_type));
if (f.source_file) free(const_cast<char *>(f.source_file));
for (int j = 0; j < f.num_params; j++) {
free(const_cast<char *>(f.params[j].name));
free(const_cast<char *>(f.params[j].type_spelling));
}
free(f.params);
}
free(info->functions);
free(info);
}
/* ------------------------------------------------------------------ */
/* C source compilation to LLVM module */
/* ------------------------------------------------------------------ */
extern "C" LLVMModuleRef sx_clang_compile_to_module(
LLVMContextRef ctx_ref,
const char *filename,
const char **args, int num_args,
char **out_error)
{
llvm::LLVMContext &ctx = *llvm::unwrap(ctx_ref);
llvm::SmallVector<const char *, 4> extra;
auto CI = buildCompilerInstance(filename, args, num_args, extra, out_error);
if (!CI) return nullptr;
clang::EmitLLVMOnlyAction action(&ctx);
if (!CI->ExecuteAction(action)) {
if (out_error) *out_error = strdup("clang compilation failed");
return nullptr;
}
std::unique_ptr<llvm::Module> mod = action.takeModule();
if (!mod) {
if (out_error) *out_error = strdup("no module produced");
return nullptr;
}
return llvm::wrap(mod.release());
}
/* ------------------------------------------------------------------ */
/* C source compilation to native object code */
/* ------------------------------------------------------------------ */
extern "C" LLVMMemoryBufferRef sx_clang_compile_to_object(
const char *filename,
const char **args, int num_args,
char **out_error)
{
// Initialize LLVM targets (idempotent)
llvm::InitializeAllTargets();
llvm::InitializeAllTargetMCs();
llvm::InitializeAllAsmPrinters();
llvm::InitializeAllAsmParsers();
// Use a local context — the module is temporary, only .o bytes are kept
llvm::LLVMContext ctx;
llvm::SmallVector<const char *, 4> extra;
extra.push_back("-fPIC");
auto CI = buildCompilerInstance(filename, args, num_args, extra, out_error);
if (!CI) return nullptr;
clang::EmitLLVMOnlyAction action(&ctx);
if (!CI->ExecuteAction(action)) {
if (out_error) *out_error = strdup("clang compilation failed");
return nullptr;
}
std::unique_ptr<llvm::Module> mod = action.takeModule();
if (!mod) {
if (out_error) *out_error = strdup("no module produced");
return nullptr;
}
// Compile LLVM module to native object code
std::string triple = mod->getTargetTriple();
std::string err_str;
const llvm::Target *target = llvm::TargetRegistry::lookupTarget(triple, err_str);
if (!target) {
if (out_error) *out_error = strdup(("target lookup failed: " + err_str).c_str());
return nullptr;
}
llvm::TargetOptions opts;
auto TM = std::unique_ptr<llvm::TargetMachine>(
target->createTargetMachine(triple, "generic", "", opts,
llvm::Reloc::PIC_));
if (!TM) {
if (out_error) *out_error = strdup("failed to create target machine");
return nullptr;
}
mod->setDataLayout(TM->createDataLayout());
llvm::SmallVector<char, 0> obj_buf;
llvm::raw_svector_ostream OS(obj_buf);
llvm::legacy::PassManager PM;
if (TM->addPassesToEmitFile(PM, OS, nullptr,
llvm::CodeGenFileType::ObjectFile)) {
if (out_error) *out_error = strdup("target cannot emit object file");
return nullptr;
}
PM.run(*mod);
// Return as LLVMMemoryBufferRef
auto buf = llvm::MemoryBuffer::getMemBufferCopy(
llvm::StringRef(obj_buf.data(), obj_buf.size()), "c_import.o");
return llvm::wrap(buf.release());
}

57
clang_shim.h Normal file
View File

@@ -0,0 +1,57 @@
#ifndef SX_CLANG_SHIM_H
#define SX_CLANG_SHIM_H
#include <llvm-c/Core.h>
#ifdef __cplusplus
extern "C" {
#endif
/* --- Header parsing --- */
typedef struct {
const char *name;
const char *type_spelling;
} SxCParamInfo;
typedef struct {
const char *name;
const char *return_type;
SxCParamInfo *params;
int num_params;
const char *source_file;
unsigned source_line;
} SxCFunctionInfo;
typedef struct {
SxCFunctionInfo *functions;
int num_functions;
} SxCHeaderInfo;
SxCHeaderInfo *sx_clang_parse_header(
const char *filename,
const char **args, int num_args,
char **out_error);
void sx_clang_free_header_info(SxCHeaderInfo *info);
/* --- C source compilation to LLVM module --- */
LLVMModuleRef sx_clang_compile_to_module(
LLVMContextRef ctx,
const char *filename,
const char **args, int num_args,
char **out_error);
/* --- C source compilation to native object code --- */
LLVMMemoryBufferRef sx_clang_compile_to_object(
const char *filename,
const char **args, int num_args,
char **out_error);
#ifdef __cplusplus
}
#endif
#endif /* SX_CLANG_SHIM_H */

View File

@@ -194,7 +194,7 @@ main :: () {
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, 1152, xx vertices, GL_STATIC_DRAW);
glBufferData(GL_ARRAY_BUFFER, 1152, @vertices, GL_STATIC_DRAW);
// Position attribute (location 0): 3 floats, stride 32 bytes, offset 0
glVertexAttribPointer(0, 3, GL_FLOAT, 0, 32, xx 0);

14
examples/33-c-import.sx Normal file
View File

@@ -0,0 +1,14 @@
#import "modules/std.sx";
#import c {
#include "vendors/test_c/test.h";
#source "vendors/test_c/test.c";
};
main :: () -> s32 {
a := add_numbers(10, 20);
b := multiply(5, 6);
print("add_numbers(10, 20) = {}\n", a);
print("multiply(5, 6) = {}\n", b);
0;
}

16
examples/33-stb-image.sx Normal file
View File

@@ -0,0 +1,16 @@
#import "modules/std.sx";
stb :: #import "modules/stb.sx";
main :: () -> s32 {
w: s32 = 0;
h: s32 = 0;
ch: s32 = 0;
img := stb.stbi_load("test.png", @w, @h, @ch, 4);
if xx img != 0 {
print("loaded {}x{} ({} channels)\n", w, h, ch);
stb.stbi_image_free(xx img);
} else {
print("no image (expected in test)\n");
}
0;
}

View File

@@ -0,0 +1,10 @@
#import "modules/std.sx";
tc :: #import "modules/test_c.sx";
main :: () -> s32 {
a := tc.add_numbers(10, 20);
b := tc.multiply(5, 6);
print("tc.add_numbers(10, 20) = {}\n", a);
print("tc.multiply(5, 6) = {}\n", b);
0;
}

4
examples/modules/stb.sx Normal file
View File

@@ -0,0 +1,4 @@
#import c {
#include "vendors/stb_image/stb_image.h";
#source "vendors/stb_image/stb_image_impl.c";
};

View File

@@ -0,0 +1,4 @@
#import c {
#include "vendors/test_c/test.h";
#source "vendors/test_c/test.c";
};

4
modules/test_c.sx Normal file
View File

@@ -0,0 +1,4 @@
#import c {
#include "vendors/test_c/test.h";
#source "vendors/test_c/test.c";
};

View File

@@ -68,6 +68,7 @@ pub const Node = struct {
tuple_type_expr: TupleTypeExpr,
tuple_literal: TupleLiteral,
ufcs_alias: UfcsAlias,
c_import_decl: CImportDecl,
pub fn declName(self: Data) ?[]const u8 {
return switch (self) {
@@ -79,6 +80,7 @@ pub const Node = struct {
.union_decl => |d| d.name,
.namespace_decl => |d| d.name,
.ufcs_alias => |d| d.name,
.c_import_decl => |d| d.name,
else => null,
};
}
@@ -427,3 +429,12 @@ pub const UfcsAlias = struct {
name: []const u8,
target: []const u8,
};
pub const CImportDecl = struct {
includes: []const []const u8,
sources: []const []const u8,
defines: []const []const u8,
flags: []const []const u8,
name: ?[]const u8 = null,
bitcode_paths: []const []const u8 = &.{}, // populated during import resolution
};

517
src/c_import.zig Normal file
View File

@@ -0,0 +1,517 @@
const std = @import("std");
const ast = @import("ast.zig");
const llvm = @import("llvm_api.zig");
const Node = ast.Node;
const c = llvm.c;
const builtin = @import("builtin");
pub const CSourceLocation = struct {
file: []const u8,
line: u32,
};
pub const CImportResult = struct {
fn_decls: []const *Node,
/// Source locations for each fn_decl (parallel array, same indices).
locations: []const CSourceLocation,
};
/// Info collected from c_import_decl AST nodes for native compilation.
pub const CImportInfo = struct {
sources: []const []const u8,
includes: []const []const u8,
defines: []const []const u8,
flags: []const []const u8,
};
/// Handle returned from loadCObjectsForJIT — caller must call unload() after JIT.
pub const CImportHandle = struct {
dylib_handle: ?*anyopaque = null,
temp_paths: []const []const u8 = &.{},
allocator: std.mem.Allocator,
pub fn unload(self: *CImportHandle, io: std.Io) void {
// dlclose
if (self.dylib_handle) |h| {
_ = std.c.dlclose(h);
}
// Clean up temp files
for (self.temp_paths) |path| {
std.Io.Dir.deleteFile(.cwd(), io, path) catch {};
}
}
};
/// Parse C headers to extract function declarations as synthetic AST nodes.
/// Called during import resolution (no LLVM context needed).
pub fn processCImport(
allocator: std.mem.Allocator,
includes: []const []const u8,
defines: []const []const u8,
flags: []const []const u8,
) !CImportResult {
// Build clang args: -I dirs, -D defines, raw flags
var args_list = std.ArrayList([*c]const u8).empty;
for (includes) |inc| {
const dir = dirName(inc);
const arg = try allocPrintZ(allocator, "-I{s}", .{dir});
try args_list.append(allocator, arg.ptr);
}
for (defines) |def| {
const arg = try allocPrintZ(allocator, "-D{s}", .{def});
try args_list.append(allocator, arg.ptr);
}
for (flags) |flag| {
const arg = try allocator.dupeZ(u8, flag);
try args_list.append(allocator, arg.ptr);
}
var all_decls = std.ArrayList(*Node).empty;
var all_locs = std.ArrayList(CSourceLocation).empty;
for (includes) |header| {
const header_z = try allocator.dupeZ(u8, header);
var err_msg: [*c]u8 = null;
const args_ptr: [*c][*c]const u8 = if (args_list.items.len > 0)
@ptrCast(args_list.items.ptr)
else
null;
const info = c.sx_clang_parse_header(
header_z.ptr,
args_ptr,
@intCast(args_list.items.len),
&err_msg,
);
if (info == null) {
if (err_msg) |e| {
std.debug.print("clang parse error for '{s}': {s}\n", .{ header, std.mem.span(e) });
}
return error.CompileError;
}
defer c.sx_clang_free_header_info(info);
const funcs = info.*.functions;
const num: usize = @intCast(info.*.num_functions);
for (0..num) |i| {
const fi = funcs[i];
const name = try allocator.dupe(u8, std.mem.span(fi.name));
// Build params
var params = std.ArrayList(ast.Param).empty;
const np: usize = @intCast(fi.num_params);
for (0..np) |j| {
const pi = fi.params[j];
const pname_raw = std.mem.span(pi.name);
const pname = if (pname_raw.len > 0)
try allocator.dupe(u8, pname_raw)
else
try std.fmt.allocPrint(allocator, "p{d}", .{j});
const ptype_str = std.mem.span(pi.type_spelling);
const ptype_node = try mapCTypeToSxNode(allocator, ptype_str);
try params.append(allocator, .{
.name = pname,
.name_span = .{ .start = 0, .end = 0 },
.type_expr = ptype_node,
});
}
// Return type
const ret_str = std.mem.span(fi.return_type);
const ret_node = if (std.mem.eql(u8, ret_str, "void"))
null
else
try mapCTypeToSxNode(allocator, ret_str);
// Create foreign_expr body (no library_ref — symbols resolved at runtime)
const foreign_body = try allocator.create(Node);
foreign_body.* = .{
.span = .{ .start = 0, .end = 0 },
.data = .{ .foreign_expr = .{ .library_ref = null, .c_name = null } },
};
const fn_node = try allocator.create(Node);
fn_node.* = .{
.span = .{ .start = 0, .end = 0 },
.data = .{ .fn_decl = .{
.name = name,
.params = try params.toOwnedSlice(allocator),
.return_type = ret_node,
.body = foreign_body,
} },
};
try all_decls.append(allocator, fn_node);
// Collect source location
const src_file = if (fi.source_file) |sf|
try allocator.dupe(u8, std.mem.span(sf))
else
header;
try all_locs.append(allocator, .{
.file = src_file,
.line = @intCast(fi.source_line),
});
}
}
return .{
.fn_decls = try all_decls.toOwnedSlice(allocator),
.locations = try all_locs.toOwnedSlice(allocator),
};
}
// ---------------------------------------------------------------------------
// Native C compilation (compile to .o, not LLVM module)
// ---------------------------------------------------------------------------
/// Compile C sources to native object files (in memory).
/// Returns list of LLVMMemoryBufferRef (each containing a .o file).
pub fn compileCToObjects(
allocator: std.mem.Allocator,
infos: []const CImportInfo,
) ![]c.LLVMMemoryBufferRef {
var obj_bufs = std.ArrayList(c.LLVMMemoryBufferRef).empty;
for (infos) |info| {
if (info.sources.len == 0) continue;
// Build clang args: -I dirs, -D defines, raw flags
var args_list = std.ArrayList([*c]const u8).empty;
for (info.includes) |inc| {
const dir = dirName(inc);
try args_list.append(allocator, (try allocPrintZ(allocator, "-I{s}", .{dir})).ptr);
}
for (info.defines) |def| {
try args_list.append(allocator, (try allocPrintZ(allocator, "-D{s}", .{def})).ptr);
}
for (info.flags) |flag| {
try args_list.append(allocator, (try allocator.dupeZ(u8, flag)).ptr);
}
const args_ptr: [*c][*c]const u8 = if (args_list.items.len > 0)
@ptrCast(args_list.items.ptr)
else
null;
const args_len: c_int = @intCast(args_list.items.len);
for (info.sources) |src| {
const src_z = try allocator.dupeZ(u8, src);
var err_msg: [*c]u8 = null;
const obj_buf = c.sx_clang_compile_to_object(
src_z.ptr,
args_ptr,
args_len,
&err_msg,
);
if (obj_buf == null) {
if (err_msg) |e| {
std.debug.print("clang compile error for '{s}': {s}\n", .{ src, std.mem.span(e) });
}
return error.CompileError;
}
try obj_bufs.append(allocator, obj_buf);
}
}
return try obj_bufs.toOwnedSlice(allocator);
}
/// For JIT mode: write .o files to temp, link into a shared library, dlopen it.
/// Returns a handle that must be unloaded after JIT execution.
pub fn loadCObjectsForJIT(
allocator: std.mem.Allocator,
io: std.Io,
obj_bufs: []c.LLVMMemoryBufferRef,
) !CImportHandle {
if (obj_bufs.len == 0) return .{ .allocator = allocator };
var temp_paths = std.ArrayList([]const u8).empty;
// Write each .o buffer to a temp file
var obj_paths = std.ArrayList([]const u8).empty;
for (obj_bufs, 0..) |buf, i| {
const path = try std.fmt.allocPrint(allocator, "/tmp/sx_c_{d}.o", .{i});
const start = c.LLVMGetBufferStart(buf);
const size = c.LLVMGetBufferSize(buf);
const data = @as([*]const u8, @ptrCast(start))[0..size];
std.Io.Dir.writeFile(.cwd(), io, .{ .sub_path = path, .data = data }) catch {
std.debug.print("failed to write temp object: {s}\n", .{path});
return error.CompileError;
};
try obj_paths.append(allocator, path);
try temp_paths.append(allocator, path);
c.LLVMDisposeMemoryBuffer(buf);
}
// Link into a shared library
const dylib_path = "/tmp/sx_c_import.dylib";
try temp_paths.append(allocator, try allocator.dupe(u8, dylib_path));
var argv = std.ArrayList([]const u8).empty;
try argv.append(allocator, "cc");
if (comptime builtin.os.tag == .macos) {
try argv.append(allocator, "-dynamiclib");
} else {
try argv.append(allocator, "-shared");
}
try argv.append(allocator, "-o");
try argv.append(allocator, dylib_path);
for (obj_paths.items) |op| {
try argv.append(allocator, op);
}
const argv_slice = try argv.toOwnedSlice(allocator);
var child = std.process.spawn(io, .{
.argv = argv_slice,
}) catch {
std.debug.print("failed to spawn linker for C import shared library\n", .{});
return error.CompileError;
};
const result = child.wait(io) catch {
std.debug.print("linker wait failed for C import shared library\n", .{});
return error.CompileError;
};
if (result != .exited or result.exited != 0) {
std.debug.print("linker failed for C import shared library (exit={})\n", .{result.exited});
return error.CompileError;
}
// dlopen the shared library
const dylib_z = try allocator.dupeZ(u8, dylib_path);
const handle = std.c.dlopen(dylib_z.ptr, .{ .NOW = true });
if (handle == null) {
const err = std.c.dlerror();
if (err) |e| {
std.debug.print("dlopen failed: {s}\n", .{std.mem.span(e)});
}
return error.CompileError;
}
return .{
.dylib_handle = handle,
.temp_paths = try temp_paths.toOwnedSlice(allocator),
.allocator = allocator,
};
}
/// For build mode: write .o buffers to temp files, return paths for the linker.
pub fn writeCObjectFiles(
allocator: std.mem.Allocator,
io: std.Io,
obj_bufs: []c.LLVMMemoryBufferRef,
) ![]const []const u8 {
var paths = std.ArrayList([]const u8).empty;
for (obj_bufs, 0..) |buf, i| {
const path = try std.fmt.allocPrint(allocator, "/tmp/sx_c_{d}.o", .{i});
const start = c.LLVMGetBufferStart(buf);
const size = c.LLVMGetBufferSize(buf);
const data = @as([*]const u8, @ptrCast(start))[0..size];
std.Io.Dir.writeFile(.cwd(), io, .{ .sub_path = path, .data = data }) catch {
std.debug.print("failed to write temp object: {s}\n", .{path});
return error.CompileError;
};
try paths.append(allocator, path);
c.LLVMDisposeMemoryBuffer(buf);
}
return try paths.toOwnedSlice(allocator);
}
/// Walk the resolved AST and collect CImportInfo from all c_import_decl nodes.
pub fn collectCImportSources(allocator: std.mem.Allocator, root: *const Node) ![]CImportInfo {
if (root.data != .root) return &.{};
var infos = std.ArrayList(CImportInfo).empty;
for (root.data.root.decls) |decl| {
switch (decl.data) {
.c_import_decl => |ci| {
if (ci.sources.len > 0) {
try infos.append(allocator, .{
.sources = ci.sources,
.includes = ci.includes,
.defines = ci.defines,
.flags = ci.flags,
});
}
},
.namespace_decl => |ns| {
for (ns.decls) |nd| {
if (nd.data == .c_import_decl) {
const nci = nd.data.c_import_decl;
if (nci.sources.len > 0) {
try infos.append(allocator, .{
.sources = nci.sources,
.includes = nci.includes,
.defines = nci.defines,
.flags = nci.flags,
});
}
}
}
},
else => {},
}
}
return try infos.toOwnedSlice(allocator);
}
// ---------------------------------------------------------------------------
// C type → sx type mapping
// ---------------------------------------------------------------------------
fn mapCTypeToSxNode(
allocator: std.mem.Allocator,
c_type: []const u8,
) !*Node {
const trimmed = std.mem.trim(u8, c_type, " ");
// Pointer types (trailing *)
if (std.mem.endsWith(u8, trimmed, "*")) {
const base = std.mem.trim(u8, trimmed[0 .. trimmed.len - 1], " ");
// const char * → [*]u8 (raw pointer, matches C ABI)
if (std.mem.eql(u8, base, "const char") or std.mem.eql(u8, base, "char const")) {
return makeManyPointerTypeNode(allocator, "u8");
}
// char * → [*]u8
if (std.mem.eql(u8, base, "char")) {
return makeManyPointerTypeNode(allocator, "u8");
}
// unsigned char * / const unsigned char * → [*]u8
if (std.mem.eql(u8, base, "unsigned char") or
std.mem.eql(u8, base, "const unsigned char") or
std.mem.eql(u8, base, "unsigned char const"))
{
return makeManyPointerTypeNode(allocator, "u8");
}
// void * / const void * → *void
if (std.mem.eql(u8, base, "void") or std.mem.eql(u8, base, "const void")) {
return makePointerTypeNode(allocator, "void");
}
// int * → *s32
if (std.mem.eql(u8, base, "int") or std.mem.eql(u8, base, "const int")) {
return makePointerTypeNode(allocator, "s32");
}
// unsigned int * / unsigned * → *u32
if (std.mem.eql(u8, base, "unsigned int") or std.mem.eql(u8, base, "unsigned") or std.mem.eql(u8, base, "const unsigned int")) {
return makePointerTypeNode(allocator, "u32");
}
// float * → *f32
if (std.mem.eql(u8, base, "float") or std.mem.eql(u8, base, "const float")) {
return makePointerTypeNode(allocator, "f32");
}
// double * → *f64
if (std.mem.eql(u8, base, "double") or std.mem.eql(u8, base, "const double")) {
return makePointerTypeNode(allocator, "f64");
}
// short * → *s16
if (std.mem.eql(u8, base, "short") or std.mem.eql(u8, base, "const short")) {
return makePointerTypeNode(allocator, "s16");
}
// Pointer to pointer → *void
if (std.mem.endsWith(u8, base, "*")) {
return makePointerTypeNode(allocator, "void");
}
// Remove const qualifier and retry
if (std.mem.startsWith(u8, base, "const ")) {
const without_const = try std.fmt.allocPrint(allocator, "{s} *", .{base[6..]});
return mapCTypeToSxNode(allocator, without_const);
}
// Default: struct/opaque pointer → *void
return makePointerTypeNode(allocator, "void");
}
// Direct types
if (std.mem.eql(u8, trimmed, "int") or std.mem.eql(u8, trimmed, "signed int")) return makeTypeExprNode(allocator, "s32");
if (std.mem.eql(u8, trimmed, "unsigned int") or std.mem.eql(u8, trimmed, "unsigned")) return makeTypeExprNode(allocator, "u32");
if (std.mem.eql(u8, trimmed, "long") or std.mem.eql(u8, trimmed, "long int") or std.mem.eql(u8, trimmed, "signed long")) return makeTypeExprNode(allocator, "s64");
if (std.mem.eql(u8, trimmed, "unsigned long") or std.mem.eql(u8, trimmed, "unsigned long int")) return makeTypeExprNode(allocator, "u64");
if (std.mem.eql(u8, trimmed, "long long") or std.mem.eql(u8, trimmed, "long long int")) return makeTypeExprNode(allocator, "s64");
if (std.mem.eql(u8, trimmed, "unsigned long long") or std.mem.eql(u8, trimmed, "unsigned long long int")) return makeTypeExprNode(allocator, "u64");
if (std.mem.eql(u8, trimmed, "short") or std.mem.eql(u8, trimmed, "short int") or std.mem.eql(u8, trimmed, "signed short")) return makeTypeExprNode(allocator, "s16");
if (std.mem.eql(u8, trimmed, "unsigned short") or std.mem.eql(u8, trimmed, "unsigned short int")) return makeTypeExprNode(allocator, "u16");
if (std.mem.eql(u8, trimmed, "char") or std.mem.eql(u8, trimmed, "signed char")) return makeTypeExprNode(allocator, "u8");
if (std.mem.eql(u8, trimmed, "unsigned char")) return makeTypeExprNode(allocator, "u8");
if (std.mem.eql(u8, trimmed, "float")) return makeTypeExprNode(allocator, "f32");
if (std.mem.eql(u8, trimmed, "double")) return makeTypeExprNode(allocator, "f64");
if (std.mem.eql(u8, trimmed, "size_t")) return makeTypeExprNode(allocator, "u64");
if (std.mem.eql(u8, trimmed, "_Bool") or std.mem.eql(u8, trimmed, "bool")) return makeTypeExprNode(allocator, "u8");
// Default: unknown type → s64 (treat as opaque integer-sized value)
return makeTypeExprNode(allocator, "s64");
}
// ---------------------------------------------------------------------------
// AST node construction helpers
// ---------------------------------------------------------------------------
fn makeTypeExprNode(allocator: std.mem.Allocator, name: []const u8) !*Node {
const node = try allocator.create(Node);
node.* = .{
.span = .{ .start = 0, .end = 0 },
.data = .{ .type_expr = .{ .name = name } },
};
return node;
}
fn makePointerTypeNode(allocator: std.mem.Allocator, pointee: []const u8) !*Node {
const inner = try makeTypeExprNode(allocator, pointee);
const node = try allocator.create(Node);
node.* = .{
.span = .{ .start = 0, .end = 0 },
.data = .{ .pointer_type_expr = .{ .pointee_type = inner } },
};
return node;
}
fn makeManyPointerTypeNode(allocator: std.mem.Allocator, element: []const u8) !*Node {
const inner = try makeTypeExprNode(allocator, element);
const node = try allocator.create(Node);
node.* = .{
.span = .{ .start = 0, .end = 0 },
.data = .{ .many_pointer_type_expr = .{ .element_type = inner } },
};
return node;
}
fn makeSliceTypeNode(allocator: std.mem.Allocator, element: []const u8) !*Node {
const inner = try makeTypeExprNode(allocator, element);
const node = try allocator.create(Node);
node.* = .{
.span = .{ .start = 0, .end = 0 },
.data = .{ .slice_type_expr = .{ .element_type = inner } },
};
return node;
}
// ---------------------------------------------------------------------------
// Utility
// ---------------------------------------------------------------------------
fn allocPrintZ(allocator: std.mem.Allocator, comptime fmt: []const u8, args: anytype) ![:0]u8 {
return allocator.dupeZ(u8, try std.fmt.allocPrint(allocator, fmt, args));
}
fn dirName(path: []const u8) []const u8 {
var last_sep: usize = 0;
var found = false;
for (path, 0..) |ch, i| {
if (ch == '/') {
last_sep = i;
found = true;
}
}
return if (found) path[0..last_sep] else ".";
}

View File

@@ -8498,6 +8498,32 @@ pub const CodeGen = struct {
return self.inferType(node).isFloat();
}
/// Load LLVM bitcode from a file and merge it into the current module.
pub fn mergeBitcodeFile(self: *CodeGen, bc_path: []const u8) !void {
const bc_path_z = try self.allocator.dupeZ(u8, bc_path);
var buf: c.LLVMMemoryBufferRef = null;
var err_msg: [*c]u8 = null;
if (c.LLVMCreateMemoryBufferWithContentsOfFile(bc_path_z.ptr, &buf, &err_msg) != 0) {
if (err_msg != null) {
defer c.LLVMDisposeMessage(err_msg);
const msg = std.mem.span(err_msg);
return self.emitErrorFmt("failed to read bitcode '{s}': {s}", .{ bc_path, msg });
}
return error.CompileError;
}
var bc_module: c.LLVMModuleRef = null;
if (c.LLVMParseBitcodeInContext2(self.context, buf, &bc_module) != 0) {
return self.emitErrorFmt("failed to parse bitcode '{s}'", .{bc_path});
}
// LLVMLinkModules2 destroys bc_module on success
if (c.LLVMLinkModules2(self.module, bc_module) != 0) {
return self.emitErrorFmt("failed to link bitcode module '{s}'", .{bc_path});
}
}
pub fn verify(self: *CodeGen) !void {
var err_msg: [*c]u8 = null;
if (c.LLVMVerifyModule(self.module, c.LLVMReturnStatusAction, &err_msg) != 0) {
@@ -8602,13 +8628,14 @@ pub const CodeGen = struct {
return if (result >= 0 and result <= 255) @intCast(result) else 1;
}
pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, output_bin: []const u8, libraries: []const []const u8, target_config: TargetConfig) !void {
pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, extra_objects: []const []const u8, output_bin: []const u8, libraries: []const []const u8, target_config: TargetConfig) !void {
var argv = std.ArrayList([]const u8).empty;
if (target_config.isWindows()) {
// Windows: MSVC-style linker flags
const linker = target_config.linker orelse "link.exe";
try argv.appendSlice(allocator, &.{ linker, output_obj });
for (extra_objects) |eo| try argv.append(allocator, eo);
try argv.append(allocator, try std.fmt.allocPrint(allocator, "/OUT:{s}", .{output_bin}));
for (target_config.lib_paths) |lp| {
@@ -8620,6 +8647,7 @@ pub const CodeGen = struct {
} else {
// Unix: cc-style linker flags
try argv.appendSlice(allocator, &.{ target_config.getLinker(), output_obj, "-o", output_bin });
for (extra_objects) |eo| try argv.append(allocator, eo);
if (target_config.sysroot) |sr| {
try argv.append(allocator, try std.fmt.allocPrint(allocator, "--sysroot={s}", .{sr}));

View File

@@ -5,6 +5,7 @@ const imports = @import("imports.zig");
const sema = @import("sema.zig");
const codegen = @import("codegen.zig");
const errors = @import("errors.zig");
const c_import = @import("c_import.zig");
const Node = ast.Node;
pub const TargetConfig = codegen.TargetConfig;
@@ -93,9 +94,17 @@ pub const Compilation = struct {
cg.sema_result = sr;
}
cg.generate(root) catch return error.CompileError;
self.cg = cg;
}
/// Collect C import source info from the resolved AST.
/// Called after generateCode() to compile C sources natively (not merged into LLVM module).
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);
}
pub fn renderErrors(self: *const Compilation) void {
self.diagnostics.renderDebug();
}

View File

@@ -2,6 +2,7 @@ const std = @import("std");
const ast = @import("ast.zig");
const parser = @import("parser.zig");
const errors = @import("errors.zig");
const c_import = @import("c_import.zig");
const Node = ast.Node;
pub fn dirName(path: []const u8) []const u8 {
@@ -87,6 +88,50 @@ pub fn resolveImports(
var decl_list = std.ArrayList(*Node).empty;
for (root.data.root.decls) |decl| {
if (decl.data == .c_import_decl) {
const ci = decl.data.c_import_decl;
// Parse headers to get synthetic function declarations
const result = c_import.processCImport(
allocator,
ci.includes,
ci.defines,
ci.flags,
) catch |err| {
if (diagnostics) |diags| {
diags.addFmt(.err, decl.span, "#import c failed: {}", .{err});
}
return error.ImportError;
};
if (ci.name) |ns_name| {
// Namespaced: wrap fn_decls + c_import_decl in a namespace
var ns_decls = std.ArrayList(*Node).empty;
for (result.fn_decls) |fd| {
try ns_decls.append(allocator, fd);
}
// Keep c_import_decl inside namespace so codegen can find sources
try ns_decls.append(allocator, decl);
const ns_node = try allocator.create(Node);
ns_node.* = .{
.span = decl.span,
.data = .{ .namespace_decl = .{
.name = ns_name,
.decls = try ns_decls.toOwnedSlice(allocator),
} },
};
try mod.scope.put(ns_name, {});
try decl_list.append(allocator, ns_node);
} else {
// Flat: add fn_decls directly + keep c_import_decl
for (result.fn_decls) |fd| {
_ = try mod.addDecl(allocator, &decl_list, fd);
}
_ = try mod.addDecl(allocator, &decl_list, decl);
}
continue;
}
if (decl.data != .import_decl) {
_ = try mod.addDecl(allocator, &decl_list, decl);
continue;

View File

@@ -72,6 +72,10 @@ pub const Lexer = struct {
.{ "#foreign", Tag.hash_foreign },
.{ "#library", Tag.hash_library },
.{ "#using", Tag.hash_using },
.{ "#include", Tag.hash_include },
.{ "#source", Tag.hash_source },
.{ "#define", Tag.hash_define },
.{ "#flags", Tag.hash_flags },
};
inline for (directives) |d| {
const keyword = d[0];

View File

@@ -7,6 +7,11 @@ pub const c = @cImport({
@cInclude("llvm-c/LLJIT.h");
@cInclude("llvm-c/Orc.h");
@cInclude("llvm-c/Error.h");
@cInclude("llvm-c/BitReader.h");
@cInclude("llvm-c/Linker.h");
// Clang shim for C header parsing + source compilation
@cInclude("clang_shim.h");
});
extern fn sx_llvm_init_all_targets() void;

View File

@@ -4,6 +4,7 @@ const sx = struct {
pub const parser = @import("../parser.zig");
pub const sema = @import("../sema.zig");
pub const imports = @import("../imports.zig");
pub const c_import = @import("../c_import.zig");
};
pub const Import = struct {
@@ -28,6 +29,10 @@ pub const Document = struct {
last_good_sema: ?sx.sema.SemaResult = null,
/// Import declarations parsed from this file.
imports: []const Import,
/// Last successful imports (preserved across parse failures for completions).
last_good_imports: []const Import = &.{},
/// Source locations for C import functions (name → file:line for go-to-definition).
c_source_locations: std.StringHashMap(sx.c_import.CSourceLocation),
/// True while this document is being analyzed (circular import guard).
is_analyzing: bool = false,
@@ -110,6 +115,7 @@ pub const DocumentStore = struct {
.root = null,
.sema = null,
.imports = &.{},
.c_source_locations = std.StringHashMap(sx.c_import.CSourceLocation).init(self.allocator),
};
try self.by_path.put(path_owned, doc);
return doc;
@@ -126,6 +132,43 @@ pub const DocumentStore = struct {
var p = sx.parser.Parser.init(self.allocator, doc.source);
doc.root = p.parse() catch return;
}
// Expand root with synthetic fn_decls from #import c { ... } declarations.
// This makes C functions visible to sema, completions, and hover.
doc.c_source_locations = std.StringHashMap(sx.c_import.CSourceLocation).init(self.allocator);
if (doc.root) |parsed_root| {
if (parsed_root.data == .root) {
var expanded = std.ArrayList(*sx.ast.Node).empty;
for (parsed_root.data.root.decls) |decl| {
if (decl.data == .c_import_decl) {
const ci = decl.data.c_import_decl;
if (sx.c_import.processCImport(
self.allocator,
ci.includes,
ci.defines,
ci.flags,
)) |result| {
for (result.fn_decls, result.locations) |fd, loc| {
try expanded.append(self.allocator, fd);
if (fd.data == .fn_decl) {
try doc.c_source_locations.put(fd.data.fn_decl.name, loc);
}
}
} else |_| {}
}
try expanded.append(self.allocator, decl);
}
if (expanded.items.len != parsed_root.data.root.decls.len) {
const new_root = try self.allocator.create(sx.ast.Node);
new_root.* = .{
.span = parsed_root.span,
.data = .{ .root = .{ .decls = try expanded.toOwnedSlice(self.allocator) } },
};
doc.root = new_root;
}
}
}
const root = doc.root orelse return;
// Extract imports from AST
@@ -146,6 +189,7 @@ pub const DocumentStore = struct {
}
}
doc.imports = try import_list.toOwnedSlice(self.allocator);
doc.last_good_imports = doc.imports;
// Recursively analyze imported documents and pre-register their symbols
var analyzer = sx.sema.Analyzer.init(self.allocator);

View File

@@ -8,6 +8,7 @@ const sx = struct {
pub const sema = @import("../sema.zig");
pub const errors = @import("../errors.zig");
pub const imports = @import("../imports.zig");
pub const c_import = @import("../c_import.zig");
};
const lsp = @import("types.zig");
const doc_mod = @import("document.zig");
@@ -22,6 +23,7 @@ pub const Server = struct {
transport: *Transport,
io: std.Io,
shutdown_requested: bool = false,
root_path: []const u8 = "",
pub fn init(allocator: std.mem.Allocator, transport: *Transport, io: std.Io) Server {
return .{
@@ -44,7 +46,7 @@ pub const Server = struct {
const params = jsonGet(root, "params");
if (std.mem.eql(u8, method, "initialize")) {
self.handleInitialize(id) catch |e| self.logError(method, e);
self.handleInitialize(id, params) catch |e| self.logError(method, e);
} else if (std.mem.eql(u8, method, "initialized")) {
// Nothing to do
} else if (std.mem.eql(u8, method, "shutdown")) {
@@ -115,7 +117,19 @@ pub const Server = struct {
try self.transport.writeMessage(resp);
}
fn handleInitialize(self: *Server, id: ?std.json.Value) !void {
fn handleInitialize(self: *Server, id: ?std.json.Value, params: ?std.json.Value) !void {
// chdir to workspace root so relative paths in #import c work
chdir: {
const p = params orelse break :chdir;
const root_uri_val = jsonGet(p, "rootUri") orelse break :chdir;
const root_uri = jsonStr(root_uri_val) orelse break :chdir;
const prefix = "file://";
if (!std.mem.startsWith(u8, root_uri, prefix)) break :chdir;
const root_path = root_uri[prefix.len..];
self.root_path = self.allocator.dupe(u8, root_path) catch break :chdir;
const path_z = self.allocator.dupeZ(u8, root_path) catch break :chdir;
_ = std.c.chdir(path_z.ptr);
}
const req_id = id orelse return;
const id_json = try lsp.valueToJson(self.allocator, req_id);
const result_json = try lsp.initializeResultJson(self.allocator);
@@ -178,6 +192,10 @@ pub const Server = struct {
// Namespace import member
if (self.findImportByNs(doc, qn.ns)) |imp| {
if (self.documents.get(imp.path)) |imp_doc| {
// C import source location: jump to the C header
if (imp_doc.c_source_locations.get(qn.member)) |cloc| {
if (try self.sendCSourceLocation(id_json, cloc, qn_origin, doc.source)) return;
}
// Single-file import
if (imp_doc.sema) |imp_sema| {
if (findSymbolByName(imp_sema.symbols, qn.member)) |si| {
@@ -1029,6 +1047,10 @@ pub const Server = struct {
.hash_foreign,
.hash_library,
.hash_using,
.hash_include,
.hash_source,
.hash_define,
.hash_flags,
=> ST.keyword,
.kw_f32, .kw_f64, .kw_Type => ST.type_,
@@ -1312,6 +1334,30 @@ pub const Server = struct {
}
}
/// Send a go-to-definition response pointing to a C header source location.
fn sendCSourceLocation(self: *Server, id_json: []const u8, cloc: sx.c_import.CSourceLocation, origin_span: ?sx.ast.Span, origin_source: [:0]const u8) !bool {
// Resolve to absolute path if relative
const abs_path = if (cloc.file.len > 0 and cloc.file[0] != '/')
try std.fmt.allocPrint(self.allocator, "{s}/{s}", .{ self.root_path, cloc.file })
else
cloc.file;
const target_uri = try std.fmt.allocPrint(self.allocator, "file://{s}", .{abs_path});
const line: u32 = if (cloc.line > 0) cloc.line - 1 else 0; // LSP lines are 0-based
const target_range = lsp.Range{
.start = .{ .line = line, .character = 0 },
.end = .{ .line = line, .character = 0 },
};
if (origin_span) |os| {
const src_range = spanToRange(origin_source, os);
const loc_json = try lsp.locationLinkJson(self.allocator, target_uri, target_range, src_range);
try self.sendResponse(id_json, loc_json);
} else {
const loc_json = try lsp.locationJson(self.allocator, target_uri, target_range);
try self.sendResponse(id_json, loc_json);
}
return true;
}
/// Resolve which document a symbol belongs to (for hover/source lookup).
fn resolveSymbolDoc(self: *Server, doc: *const Document, sym: sx.sema.Symbol) *const Document {
if (sym.origin) |origin_path| {
@@ -1320,11 +1366,14 @@ pub const Server = struct {
return doc;
}
/// Find an import by namespace name.
/// Find an import by namespace name (falls back to last good imports).
fn findImportByNs(_: *Server, doc: *const Document, ns_name: []const u8) ?doc_mod.Import {
for (doc.imports) |imp| {
if (imp.ns) |ns| {
if (std.mem.eql(u8, ns, ns_name)) return imp;
const imports_lists = [_][]const doc_mod.Import{ doc.imports, doc.last_good_imports };
for (&imports_lists) |imports| {
for (imports) |imp| {
if (imp.ns) |ns| {
if (std.mem.eql(u8, ns, ns_name)) return imp;
}
}
}
return null;

View File

@@ -25,7 +25,7 @@ pub fn main(init: std.process.Init) !void {
var lib_paths = std.ArrayList([]const u8).empty;
var show_timing: bool = false;
var explicit_opt: bool = false;
var no_cache: bool = false;
var enable_cache: bool = false;
var i: usize = 2;
while (i < args.len) : (i += 1) {
@@ -60,8 +60,8 @@ pub fn main(init: std.process.Init) !void {
target_config.sysroot = args[i];
} else if (std.mem.eql(u8, arg, "--time")) {
show_timing = true;
} else if (std.mem.eql(u8, arg, "--no-cache")) {
no_cache = true;
} else if (std.mem.eql(u8, arg, "--cache")) {
enable_cache = true;
} else if (std.mem.startsWith(u8, arg, "-L")) {
if (arg.len > 2) {
try lib_paths.append(allocator, arg[2..]);
@@ -87,7 +87,7 @@ pub fn main(init: std.process.Init) !void {
if (std.mem.eql(u8, command, "build")) {
const output_name = target_config.output_path orelse deriveOutputName(path);
compile(allocator, io, path, output_name, target_config, show_timing, no_cache) catch return;
compile(allocator, io, path, output_name, target_config, show_timing, enable_cache) catch return;
std.debug.print("compiled: {s}\n", .{output_name});
} else if (std.mem.eql(u8, command, "ir")) {
emitIR(allocator, io, path, target_config) catch return;
@@ -117,7 +117,7 @@ pub fn main(init: std.process.Init) !void {
// Cache check — use .o files (precompiled object, skip IR compilation in JIT)
// Disable caching for files with top-level #run (side effects lost on cache hit)
const root = comp.resolved_root orelse comp.root orelse return;
const use_cache = !no_cache and !hasTopLevelRun(root);
const use_cache = enable_cache and !hasTopLevelRun(root);
const key = computeCacheKey(source, &comp.import_sources, target_config);
const cache_obj = cachePath(allocator, key, "o") catch return;
@@ -151,13 +151,19 @@ pub fn main(init: std.process.Init) !void {
break :blk buf;
};
// Compile C sources natively and dlopen before JIT
timer.mark();
var c_handle = compileCForJIT(allocator, io, &comp) catch { comp.renderErrors(); return; };
defer c_handle.unload(io);
timer.record("c-import");
// JIT from precompiled object (relocation only, no IR compilation)
sx.llvm_api.initNativeTarget();
timer.mark();
const exit_code = sx.codegen.CodeGen.runJITFromObject(obj_buf) catch {
// JIT failed — fall back to AOT
timer.record("jit-fail");
runAOT(allocator, io, path, target_config, &timer, no_cache) catch return;
runAOT(allocator, io, path, target_config, &timer, enable_cache) catch return;
timer.printAll();
return;
};
@@ -170,6 +176,24 @@ pub fn main(init: std.process.Init) !void {
}
}
/// Compile C sources from #import c blocks and dlopen them for JIT.
fn compileCForJIT(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Compilation) !sx.c_import.CImportHandle {
const c_infos = try comp.collectCImportSources();
if (c_infos.len == 0) return .{ .allocator = allocator };
const obj_bufs = try sx.c_import.compileCToObjects(allocator, c_infos);
return try sx.c_import.loadCObjectsForJIT(allocator, io, obj_bufs);
}
/// Compile C sources from #import c blocks to .o files for linking.
fn compileCForBuild(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Compilation) ![]const []const u8 {
const c_infos = try comp.collectCImportSources();
if (c_infos.len == 0) return &.{};
const obj_bufs = try sx.c_import.compileCToObjects(allocator, c_infos);
return try sx.c_import.writeCObjectFiles(allocator, io, obj_bufs);
}
fn parseOptLevel(s: []const u8) ?sx.codegen.TargetConfig.OptLevel {
if (std.mem.eql(u8, s, "none") or std.mem.eql(u8, s, "0")) return .none;
if (std.mem.eql(u8, s, "less") or std.mem.eql(u8, s, "1")) return .less;
@@ -197,7 +221,7 @@ fn printUsage() void {
\\ -L <path> Library search path (repeatable)
\\ --linker <cmd> Linker command (default: cc)
\\ --sysroot <path> Sysroot for cross-compilation
\\ --no-cache Disable build caching
\\ --cache Enable build caching
\\ --time Show compilation timing breakdown
\\
, .{});
@@ -232,8 +256,8 @@ fn runLsp(allocator: std.mem.Allocator, io: std.Io) void {
fn deriveOutputName(input_path: []const u8) []const u8 {
// Get basename (strip directory)
var start: usize = 0;
for (input_path, 0..) |ch, i| {
if (ch == '/' or ch == '\\') start = i + 1;
for (input_path, 0..) |ch, idx| {
if (ch == '/' or ch == '\\') start = idx + 1;
}
const basename = input_path[start..];
// Strip .sx extension
@@ -300,13 +324,13 @@ fn emitAsm(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, tar
std.debug.print("emitted: {s}\n", .{asm_path});
}
fn compile(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.codegen.TargetConfig, show_timing: bool, no_cache: bool) !void {
fn compile(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.codegen.TargetConfig, show_timing: bool, enable_cache: bool) !void {
var timer = Timing.init(show_timing);
try compileWithTimer(allocator, io, input_path, output_path, target_config, &timer, no_cache);
try compileWithTimer(allocator, io, input_path, output_path, target_config, &timer, enable_cache);
timer.printAll();
}
fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.codegen.TargetConfig, timer: *Timing, no_cache: bool) !void {
fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.codegen.TargetConfig, timer: *Timing, enable_cache: bool) !void {
// Phase A: read + parse + resolveImports (fast: ~0.5ms)
timer.mark();
const source = try readSource(allocator, io, input_path);
@@ -336,7 +360,7 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
const cache_bin = try cachePath(allocator, key, "bin");
// Level 1: Try cached binary (skip everything — no codegen, no link)
if (!no_cache) bin_cache: {
if (enable_cache) bin_cache: {
std.Io.Dir.copyFile(.cwd(), cache_bin, .cwd(), output_path, io, .{}) catch break :bin_cache;
timer.record("cache");
return;
@@ -344,7 +368,7 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
// Level 2: Try cached .o (skip codegen+emit, still need link)
const used_obj_cache = blk: {
if (no_cache) break :blk false;
if (!enable_cache) break :blk false;
std.Io.Dir.copyFile(.cwd(), cache_obj, .cwd(), obj_path, io, .{}) catch break :blk false;
break :blk true;
};
@@ -367,31 +391,42 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
timer.record("emit");
// Save .o to cache
if (!no_cache) {
if (enable_cache) {
std.Io.Dir.copyFile(.cwd(), obj_path, .cwd(), cache_obj, io, .{ .make_path = true }) catch {};
}
}
// Link
// Compile C sources from #import c blocks to .o files
timer.mark();
sx.codegen.CodeGen.link(allocator, io, obj_path, output_path, libs, target_config) catch {
const c_obj_paths = compileCForBuild(allocator, io, &comp) catch {
std.debug.print("error: C import compilation failed\n", .{});
return error.CompileError;
};
timer.record("c-import");
// Link (sx .o + C .o files)
timer.mark();
sx.codegen.CodeGen.link(allocator, io, obj_path, c_obj_paths, output_path, libs, target_config) catch {
std.debug.print("error: linking failed\n", .{});
return error.CompileError;
};
timer.record("link");
// Save linked binary to cache
if (!no_cache) {
if (enable_cache) {
std.Io.Dir.copyFile(.cwd(), output_path, .cwd(), cache_bin, io, .{ .make_path = true }) catch {};
}
// Clean up object file
// Clean up object files
std.Io.Dir.deleteFile(.cwd(), io, obj_path) catch {};
for (c_obj_paths) |cop| {
std.Io.Dir.deleteFile(.cwd(), io, cop) catch {};
}
}
fn runAOT(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.codegen.TargetConfig, timer: *Timing, no_cache: bool) !void {
fn runAOT(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.codegen.TargetConfig, timer: *Timing, enable_cache: bool) !void {
const tmp_bin = if (comptime @import("builtin").os.tag == .windows) "sx_run_tmp.exe" else "/tmp/sx_run_tmp";
try compileWithTimer(allocator, io, input_path, tmp_bin, target_config, timer, no_cache);
try compileWithTimer(allocator, io, input_path, tmp_bin, target_config, timer, enable_cache);
defer {
std.Io.Dir.deleteFile(.cwd(), io, tmp_bin) catch {};
}

View File

@@ -49,9 +49,14 @@ pub const Parser = struct {
fn parseTopLevel(self: *Parser) anyerror!*Node {
const start = self.current.loc.start;
// Top-level flat import: #import "path";
// Top-level flat import: #import "path"; or #import c { ... };
if (self.current.tag == .hash_import) {
self.advance();
// Check for #import c { ... } (C import block)
if (self.current.tag == .identifier and std.mem.eql(u8, self.tokenSlice(self.current), "c") and self.peekNext() == .l_brace) {
self.advance(); // consume 'c'
return self.parseCImportBlock(start, null);
}
if (self.current.tag != .string_literal) {
return self.fail("expected string path after '#import'");
}
@@ -105,9 +110,14 @@ pub const Parser = struct {
// After `::`
// Could be: #run expr, enum { ... }, (params) -> type { body }, or expr;
// Namespaced import: name :: #import "path";
// Namespaced import: name :: #import "path"; or name :: #import c { ... };
if (self.current.tag == .hash_import) {
self.advance();
// Check for name :: #import c { ... }
if (self.current.tag == .identifier and std.mem.eql(u8, self.tokenSlice(self.current), "c") and self.peekNext() == .l_brace) {
self.advance(); // consume 'c'
return self.parseCImportBlock(start_pos, name);
}
if (self.current.tag != .string_literal) {
return self.fail("expected string path after '#import'");
}
@@ -232,6 +242,58 @@ pub const Parser = struct {
return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = null, .value = value } });
}
fn parseCImportBlock(self: *Parser, start: u32, name: ?[]const u8) anyerror!*Node {
try self.expect(.l_brace);
var includes = std.ArrayList([]const u8).empty;
var sources = std.ArrayList([]const u8).empty;
var defines = std.ArrayList([]const u8).empty;
var flags = std.ArrayList([]const u8).empty;
while (self.current.tag != .r_brace and self.current.tag != .eof) {
if (self.current.tag == .hash_include) {
self.advance();
if (self.current.tag != .string_literal) return self.fail("expected string after '#include'");
const raw = self.tokenSlice(self.current);
try includes.append(self.allocator, raw[1 .. raw.len - 1]);
self.advance();
try self.expect(.semicolon);
} else if (self.current.tag == .hash_source) {
self.advance();
if (self.current.tag != .string_literal) return self.fail("expected string after '#source'");
const raw = self.tokenSlice(self.current);
try sources.append(self.allocator, raw[1 .. raw.len - 1]);
self.advance();
try self.expect(.semicolon);
} else if (self.current.tag == .hash_define) {
self.advance();
if (self.current.tag != .string_literal) return self.fail("expected string after '#define'");
const raw = self.tokenSlice(self.current);
try defines.append(self.allocator, raw[1 .. raw.len - 1]);
self.advance();
try self.expect(.semicolon);
} else if (self.current.tag == .hash_flags) {
self.advance();
if (self.current.tag != .string_literal) return self.fail("expected string after '#flags'");
const raw = self.tokenSlice(self.current);
try flags.append(self.allocator, raw[1 .. raw.len - 1]);
self.advance();
try self.expect(.semicolon);
} else {
return self.fail("unexpected token inside '#import c { ... }'");
}
}
try self.expect(.r_brace);
try self.expect(.semicolon);
return try self.createNode(start, .{ .c_import_decl = .{
.includes = try includes.toOwnedSlice(self.allocator),
.sources = try sources.toOwnedSlice(self.allocator),
.defines = try defines.toOwnedSlice(self.allocator),
.flags = try flags.toOwnedSlice(self.allocator),
.name = name,
} });
}
fn parseTypedBinding(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
// After `name :`
// Parse type

View File

@@ -10,6 +10,7 @@ pub const errors = @import("errors.zig");
pub const sema = @import("sema.zig");
pub const imports = @import("imports.zig");
pub const core = @import("core.zig");
pub const c_import = @import("c_import.zig");
pub const lsp = struct {
pub const server = @import("lsp/server.zig");

View File

@@ -807,6 +807,7 @@ pub const Analyzer = struct {
.library_decl,
.function_type_expr,
.import_decl,
.c_import_decl,
.array_type_expr,
.slice_type_expr,
.pointer_type_expr,
@@ -1146,6 +1147,7 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node {
.struct_decl,
.union_decl,
.import_decl,
.c_import_decl,
.array_type_expr,
.slice_type_expr,
.pointer_type_expr,

View File

@@ -96,6 +96,10 @@ pub const Tag = enum {
hash_foreign, // #foreign
hash_library, // #library
hash_using, // #using
hash_include, // #include (inside #import c { ... })
hash_source, // #source (inside #import c { ... })
hash_define, // #define (inside #import c { ... })
hash_flags, // #flags (inside #import c { ... })
triple_minus, // ---
// Special

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
add_numbers(10, 20) = 30
multiply(5, 6) = 30

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
no image (expected in test)

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
tc.add_numbers(10, 20) = 30
tc.multiply(5, 6) = 8589934622

7988
vendors/stb_image/stb_image.h vendored Normal file

File diff suppressed because it is too large Load Diff

2
vendors/stb_image/stb_image_impl.c vendored Normal file
View File

@@ -0,0 +1,2 @@
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

1724
vendors/stb_image/stb_image_write.h vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"

5079
vendors/stb_truetype/stb_truetype.h vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
#define STB_TRUETYPE_IMPLEMENTATION
#include "stb_truetype.h"

9
vendors/test_c/test.c vendored Normal file
View File

@@ -0,0 +1,9 @@
#include "test.h"
int add_numbers(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}

2
vendors/test_c/test.h vendored Normal file
View File

@@ -0,0 +1,2 @@
int add_numbers(int a, int b);
int multiply(int a, int b);