import c
This commit is contained in:
50
build.zig
50
build.zig
@@ -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
315
clang_shim.cpp
Normal 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
57
clang_shim.h
Normal 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 */
|
||||
@@ -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
14
examples/33-c-import.sx
Normal 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
16
examples/33-stb-image.sx
Normal 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;
|
||||
}
|
||||
10
examples/34-c-import-ns.sx
Normal file
10
examples/34-c-import-ns.sx
Normal 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
4
examples/modules/stb.sx
Normal file
@@ -0,0 +1,4 @@
|
||||
#import c {
|
||||
#include "vendors/stb_image/stb_image.h";
|
||||
#source "vendors/stb_image/stb_image_impl.c";
|
||||
};
|
||||
4
examples/modules/test_c.sx
Normal file
4
examples/modules/test_c.sx
Normal 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
4
modules/test_c.sx
Normal file
@@ -0,0 +1,4 @@
|
||||
#import c {
|
||||
#include "vendors/test_c/test.h";
|
||||
#source "vendors/test_c/test.c";
|
||||
};
|
||||
11
src/ast.zig
11
src/ast.zig
@@ -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
517
src/c_import.zig
Normal 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 ".";
|
||||
}
|
||||
@@ -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}));
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
77
src/main.zig
77
src/main.zig
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
1
tests/expected/33-c-import.exit
Normal file
1
tests/expected/33-c-import.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
2
tests/expected/33-c-import.txt
Normal file
2
tests/expected/33-c-import.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
add_numbers(10, 20) = 30
|
||||
multiply(5, 6) = 30
|
||||
1
tests/expected/33-stb-image.exit
Normal file
1
tests/expected/33-stb-image.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
tests/expected/33-stb-image.txt
Normal file
1
tests/expected/33-stb-image.txt
Normal file
@@ -0,0 +1 @@
|
||||
no image (expected in test)
|
||||
1
tests/expected/34-c-import-ns.exit
Normal file
1
tests/expected/34-c-import-ns.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
2
tests/expected/34-c-import-ns.txt
Normal file
2
tests/expected/34-c-import-ns.txt
Normal 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
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
2
vendors/stb_image/stb_image_impl.c
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "stb_image.h"
|
||||
1724
vendors/stb_image/stb_image_write.h
vendored
Normal file
1724
vendors/stb_image/stb_image_write.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
vendors/stb_image/stb_image_write_impl.c
vendored
Normal file
2
vendors/stb_image/stb_image_write_impl.c
vendored
Normal 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
5079
vendors/stb_truetype/stb_truetype.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
vendors/stb_truetype/stb_truetype_impl.c
vendored
Normal file
2
vendors/stb_truetype/stb_truetype_impl.c
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include "stb_truetype.h"
|
||||
9
vendors/test_c/test.c
vendored
Normal file
9
vendors/test_c/test.c
vendored
Normal 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
2
vendors/test_c/test.h
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
int add_numbers(int a, int b);
|
||||
int multiply(int a, int b);
|
||||
Reference in New Issue
Block a user