diff --git a/editors/vscode/package.json b/editors/vscode/package.json
index 224e88f..39f789d 100644
--- a/editors/vscode/package.json
+++ b/editors/vscode/package.json
@@ -10,6 +10,9 @@
"categories": [
"Programming Languages"
],
+ "activationEvents": [
+ "onLanguage:sx"
+ ],
"main": "./out/extension.js",
"contributes": {
"languages": [
diff --git a/editors/vscode/sx-lang-0.0.1.vsix b/editors/vscode/sx-lang-0.0.1.vsix
index 9a01694..a8406ee 100644
Binary files a/editors/vscode/sx-lang-0.0.1.vsix and b/editors/vscode/sx-lang-0.0.1.vsix differ
diff --git a/examples/19-varargs.sx b/examples/19-varargs.sx
index 02fedfe..d92e244 100644
--- a/examples/19-varargs.sx
+++ b/examples/19-varargs.sx
@@ -10,26 +10,26 @@ sum :: (args: ..s32) -> s32 {
print_all :: (args: ..s32) {
for args: (it) {
- write(int_to_string(it));
- write(" ");
+ out(int_to_string(it));
+ out(" ");
}
- write("\n");
+ out("\n");
}
main :: () -> s32 {
- write(int_to_string(sum(10, 20, 30)));
- write("\n");
+ out(int_to_string(sum(10, 20, 30)));
+ out("\n");
print_all(1, 2, 3, 4, 5);
arr : [3]s32 = .[10, 20, 30];
- write(int_to_string(sum(..arr)));
- write("\n");
+ out(int_to_string(sum(..arr)));
+ out("\n");
for arr: (it) {
- write(int_to_string(it));
- write(" ");
+ out(int_to_string(it));
+ out(" ");
}
- write("\n");
+ out("\n");
0;
}
diff --git a/examples/20-any-varargs.sx b/examples/20-any-varargs.sx
index c62da65..dd6ac35 100644
--- a/examples/20-any-varargs.sx
+++ b/examples/20-any-varargs.sx
@@ -10,22 +10,22 @@ print_any :: (args: ..Any) {
for args: (it) {
type := type_of(it);
if type == {
- case int: write(int_to_string(cast(s32) it));
- case string: write(cast(string) it);
- case bool: write(bool_to_string(cast(bool) it));
- case float: write(float_to_string(cast(f64) it));
+ case int: out(int_to_string(cast(s32) it));
+ case string: out(cast(string) it);
+ case bool: out(bool_to_string(cast(bool) it));
+ case float: out(float_to_string(cast(f64) it));
case Point: {
p := cast(Point) it;
- write("(");
- write(int_to_string(p.x));
- write(",");
- write(int_to_string(p.y));
- write(")");
+ out("(");
+ out(int_to_string(p.x));
+ out(",");
+ out(int_to_string(p.y));
+ out(")");
}
}
- write(" ");
+ out(" ");
}
- write("\n");
+ out("\n");
}
count :: (args: ..Any) -> s32 {
@@ -40,8 +40,8 @@ main :: () -> s32 {
print_any("point:", p, 99);
// Test count
- write(int_to_string(count(1, 2, 3)));
- write("\n");
+ out(int_to_string(count(1, 2, 3)));
+ out("\n");
0;
}
diff --git a/examples/32-http-server.sx b/examples/32-http-server.sx
new file mode 100644
index 0000000..c77dc00
--- /dev/null
+++ b/examples/32-http-server.sx
@@ -0,0 +1,63 @@
+// HTTP server example (macOS only)
+
+#import "modules/std.sx";
+#import "modules/socket.sx";
+
+main :: () -> s32 {
+ PORT :: 8080;
+
+ fd := socket(AF_INET, SOCK_STREAM, 0);
+ if fd < 0 {
+ print("error: socket()\n");
+ return 1;
+ }
+
+ opt : s32 = 1;
+ setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, @opt, 4);
+
+ addr := SockAddr.{
+ sin_len = 16,
+ sin_family = 2,
+ sin_port = htons(PORT),
+ sin_addr = 0,
+ sin_zero = 0
+ };
+
+ if bind(fd, @addr, 16) < 0 {
+ print("error: bind()\n");
+ return 1;
+ }
+
+ if listen(fd, 10) < 0 {
+ print("error: listen()\n");
+ return 1;
+ }
+
+ print("listening on http://localhost:{}\n", PORT);
+
+ while true {
+ client := accept(fd, null, null);
+ if client < 0 { continue; }
+
+ // Read request
+ buf := alloc(4096);
+ read(client, buf, 4096);
+
+ // Send response
+ body := "
Hello from sx!
";
+ response := format("HTTP/1.1 200 OK\r
+Content-Type: text/html\r
+Connection: close\r
+Content-Length: {}\r
+\r
+{}", body.len, body);
+
+ write(client, response, response.len);
+
+ close(client);
+ print(" served request\n");
+ }
+
+ close(fd);
+ 0;
+}
diff --git a/examples/50-smoke.sx b/examples/50-smoke.sx
index a45fb30..8d41f14 100644
--- a/examples/50-smoke.sx
+++ b/examples/50-smoke.sx
@@ -91,6 +91,10 @@ gen_val :: () -> string {
return "print(\"insert-gen: {}\\n\", 42);";
}
+// --- Foreign function binding ---
+libc :: #library "c";
+c_abs :: (n: s32) -> s32 #foreign libc "abs";
+
// ============================================================
main :: {
@@ -634,76 +638,76 @@ END;
// For loop basic
farr : [4]s32 = .[10, 20, 30, 40];
- write("for:");
+ out("for:");
for farr: (it) {
- write(" ");
- write(int_to_string(it));
+ out(" ");
+ out(int_to_string(it));
}
- write("\n");
+ out("\n");
// For with print
- write("for-print:");
+ out("for-print:");
for farr: (it) {
print(" {}", it);
}
- write("\n");
+ out("\n");
// For with index
- write("for-idx:");
+ out("for-idx:");
for farr: (_, ix) {
- write(" ");
- write(int_to_string(ix));
+ out(" ");
+ out(int_to_string(ix));
}
- write("\n");
+ out("\n");
// For with print two args
- write("for-2arg:");
+ out("for-2arg:");
for farr: (it, ix) {
print(" {}@{}", it, ix);
}
- write("\n");
+ out("\n");
// For with break
- write("for-break:");
+ out("for-break:");
for farr: (it) {
if it == 30 { break; }
print(" {}", it);
}
- write("\n");
+ out("\n");
// For with continue
- write("for-continue:");
+ out("for-continue:");
for farr: (it) {
if it == 20 { continue; }
print(" {}", it);
}
- write("\n");
+ out("\n");
// For on slice
fsl : []s32 = .[10, 20, 30];
- write("for-slice:");
+ out("for-slice:");
for fsl: (it) {
print(" {}", it);
}
- write("\n");
+ out("\n");
// For on slice with index
- write("for-slice-idx:");
+ out("for-slice-idx:");
for fsl: (it, ix) {
print(" {}:{}", ix, it);
}
- write("\n");
+ out("\n");
// Nested for
nf_a : [2]s32 = .[0, 1];
nf_b : [2]s32 = .[0, 1];
- write("for-nested:");
+ out("for-nested:");
for nf_a: (oa) {
for nf_b: (ob) {
print(" ({},{})", oa, ob);
}
}
- write("\n");
+ out("\n");
// For with break preserving index
fbi : [5]s32 = .[10, 20, 30, 40, 50];
@@ -857,8 +861,8 @@ END;
// ========================================================
print("=== 7. Builtins ===\n");
- // write
- write("write-ok\n");
+ // out
+ out("out-ok\n");
// sqrt
print("sqrt: {}\n", sqrt(9.0));
@@ -932,12 +936,12 @@ END;
// field_value (use any_to_string to avoid sext-on-Any bug)
fv_pt := Point.{ 11, 22 };
- write("fieldval0: ");
- write(any_to_string(field_value(fv_pt, 0)));
- write("\n");
- write("fieldval1: ");
- write(any_to_string(field_value(fv_pt, 1)));
- write("\n");
+ out("fieldval0: ");
+ out(any_to_string(field_value(fv_pt, 0)));
+ out("\n");
+ out("fieldval1: ");
+ out(any_to_string(field_value(fv_pt, 1)));
+ out("\n");
// field_index on plain enum
fi_c : Color = .green;
@@ -1039,5 +1043,13 @@ END;
print("3-way: {} {} {}\n", ra, rb, rc);
}
+ // ========================================================
+ // 15. FOREIGN FUNCTION BINDING
+ // ========================================================
+ print("=== 15. Foreign ===\n");
+
+ // Symbol rename: c_abs maps to C's abs()
+ print("foreign-rename: {}\n", c_abs(xx -42));
+
print("=== DONE ===\n");
}
diff --git a/examples/modules/raylib.sx b/examples/modules/raylib.sx
index 3cc838c..80e0d3d 100644
--- a/examples/modules/raylib.sx
+++ b/examples/modules/raylib.sx
@@ -1,4 +1,4 @@
-#library "raylib";
+raylib :: #library "raylib";
Color :: struct {
r, g, b, a: u8;
@@ -8,10 +8,10 @@ Vector2 :: struct {
x, y: f32;
}
-InitWindow :: (width: s32, height: s32, title: [:0]u8) -> void #foreign;
-CloseWindow :: () -> void #foreign;
-WindowShouldClose :: () -> bool #foreign;
-BeginDrawing :: () -> void #foreign;
-EndDrawing :: () -> void #foreign;
-ClearBackground :: (color: Color) -> void #foreign;
-DrawTriangle :: (v1: Vector2, v2: Vector2, v3: Vector2, color: Color) -> void #foreign;
+InitWindow :: (width: s32, height: s32, title: [:0]u8) -> void #foreign raylib;
+CloseWindow :: () -> void #foreign raylib;
+WindowShouldClose :: () -> bool #foreign raylib;
+BeginDrawing :: () -> void #foreign raylib;
+EndDrawing :: () -> void #foreign raylib;
+ClearBackground :: (color: Color) -> void #foreign raylib;
+DrawTriangle :: (v1: Vector2, v2: Vector2, v3: Vector2, color: Color) -> void #foreign raylib;
diff --git a/examples/modules/sdl3.sx b/examples/modules/sdl3.sx
index 1e261c3..77c0465 100644
--- a/examples/modules/sdl3.sx
+++ b/examples/modules/sdl3.sx
@@ -1,4 +1,4 @@
-#library "SDL3";
+sdl3 :: #library "SDL3";
// SDL_InitFlags
SDL_INIT_VIDEO :u32: 0x20;
@@ -317,17 +317,17 @@ SDL_Event :: enum struct { tag: u32; _: u32; payload: [30]u32; } {
}
// Functions
-SDL_Init :: (flags: u32) -> bool #foreign;
-SDL_Quit :: () -> void #foreign;
-SDL_CreateWindow :: (title: [:0]u8, w: s32, h: s32, flags: u64) -> *void #foreign;
-SDL_DestroyWindow :: (window: *void) -> void #foreign;
-SDL_GL_SetAttribute :: (attr: s32, value: s32) -> bool #foreign;
-SDL_GL_CreateContext :: (window: *void) -> *void #foreign;
-SDL_GL_DestroyContext :: (context: *void) -> bool #foreign;
-SDL_GL_MakeCurrent :: (window: *void, context: *void) -> bool #foreign;
-SDL_GL_SwapWindow :: (window: *void) -> bool #foreign;
-SDL_GL_SetSwapInterval :: (interval: s32) -> bool #foreign;
-SDL_GL_GetProcAddress :: (proc: [:0]u8) -> *void #foreign;
-SDL_PollEvent :: (event: *SDL_Event) -> bool #foreign;
-SDL_GetTicks :: () -> u64 #foreign;
-SDL_Delay :: (ms: u32) -> void #foreign;
+SDL_Init :: (flags: u32) -> bool #foreign sdl3;
+SDL_Quit :: () -> void #foreign sdl3;
+SDL_CreateWindow :: (title: [:0]u8, w: s32, h: s32, flags: u64) -> *void #foreign sdl3;
+SDL_DestroyWindow :: (window: *void) -> void #foreign sdl3;
+SDL_GL_SetAttribute :: (attr: s32, value: s32) -> bool #foreign sdl3;
+SDL_GL_CreateContext :: (window: *void) -> *void #foreign sdl3;
+SDL_GL_DestroyContext :: (context: *void) -> bool #foreign sdl3;
+SDL_GL_MakeCurrent :: (window: *void, context: *void) -> bool #foreign sdl3;
+SDL_GL_SwapWindow :: (window: *void) -> bool #foreign sdl3;
+SDL_GL_SetSwapInterval :: (interval: s32) -> bool #foreign sdl3;
+SDL_GL_GetProcAddress :: (proc: [:0]u8) -> *void #foreign sdl3;
+SDL_PollEvent :: (event: *SDL_Event) -> bool #foreign sdl3;
+SDL_GetTicks :: () -> u64 #foreign sdl3;
+SDL_Delay :: (ms: u32) -> void #foreign sdl3;
diff --git a/examples/modules/socket.sx b/examples/modules/socket.sx
new file mode 100644
index 0000000..fcacaae
--- /dev/null
+++ b/examples/modules/socket.sx
@@ -0,0 +1,33 @@
+// POSIX socket module (macOS only)
+// sockaddr_in layout and constants are platform-specific.
+
+libc :: #library "c";
+
+// POSIX socket API
+socket :: (domain: s32, kind: s32, protocol: s32) -> s32 #foreign libc;
+setsockopt :: (fd: s32, level: s32, optname: s32, optval: *s32, optlen: u32) -> s32 #foreign libc;
+bind :: (fd: s32, addr: *SockAddr, addrlen: u32) -> s32 #foreign libc;
+listen :: (fd: s32, backlog: s32) -> s32 #foreign libc;
+accept :: (fd: s32, addr: *SockAddr, addrlen: *u32) -> s32 #foreign libc;
+read :: (fd: s32, buf: [:0]u8, count: s64) -> s64 #foreign libc;
+write :: (fd: s32, buf: [:0]u8, count: s64) -> s64 #foreign libc;
+close :: (fd: s32) -> s32 #foreign libc;
+
+// Constants (macOS)
+AF_INET :s32: 2;
+SOCK_STREAM :s32: 1;
+SOL_SOCKET :s32: 0xFFFF;
+SO_REUSEADDR :s32: 0x4;
+
+// macOS sockaddr_in (16 bytes, has sin_len field)
+SockAddr :: struct {
+ sin_len: u8;
+ sin_family: u8;
+ sin_port: u16;
+ sin_addr: u32;
+ sin_zero: u64;
+}
+
+htons :: (port: s64) -> u16 {
+ cast(u16) ((port / 256) | ((port % 256) * 256));
+}
diff --git a/examples/modules/std.sx b/examples/modules/std.sx
index 7927fd3..f4d3df2 100644
--- a/examples/modules/std.sx
+++ b/examples/modules/std.sx
@@ -1,5 +1,5 @@
Vector :: ($N: int, $T: Type) -> Type #builtin;
-write :: (str: string) -> void #builtin;
+out :: (str: string) -> void #builtin;
sqrt :: (x: $T) -> T #builtin;
sin :: (x: $T) -> T #builtin;
cos :: (x: $T) -> T #builtin;
@@ -315,10 +315,81 @@ build_print :: (fmt: string) -> string {
code = concat(code, int_to_string(fmt.len - seg_start));
code = concat(code, ")); ");
}
- code = concat(code, "write(result);");
+ code = concat(code, "out(result);");
code;
}
+build_format :: (fmt: string) -> string {
+ code := "result := \"\"; ";
+ seg_start := 0;
+ i := 0;
+ arg_idx := 0;
+ while i < fmt.len {
+ if fmt[i] == 123 {
+ if i + 1 < fmt.len {
+ if fmt[i + 1] == 125 {
+ if i > seg_start {
+ code = concat(code, "result = concat(result, substr(fmt, ");
+ code = concat(code, int_to_string(seg_start));
+ code = concat(code, ", ");
+ code = concat(code, int_to_string(i - seg_start));
+ code = concat(code, ")); ");
+ }
+ code = concat(code, "result = concat(result, any_to_string(args[");
+ code = concat(code, int_to_string(arg_idx));
+ code = concat(code, "])); ");
+ arg_idx += 1;
+ i += 2;
+ seg_start = i;
+ } else if fmt[i + 1] == 123 {
+ code = concat(code, "result = concat(result, substr(fmt, ");
+ code = concat(code, int_to_string(seg_start));
+ code = concat(code, ", ");
+ code = concat(code, int_to_string(i - seg_start + 1));
+ code = concat(code, ")); ");
+ i += 2;
+ seg_start = i;
+ } else {
+ i += 1;
+ }
+ } else {
+ i += 1;
+ }
+ } else if fmt[i] == 125 {
+ if i + 1 < fmt.len {
+ if fmt[i + 1] == 125 {
+ code = concat(code, "result = concat(result, substr(fmt, ");
+ code = concat(code, int_to_string(seg_start));
+ code = concat(code, ", ");
+ code = concat(code, int_to_string(i - seg_start + 1));
+ code = concat(code, ")); ");
+ i += 2;
+ seg_start = i;
+ } else {
+ i += 1;
+ }
+ } else {
+ i += 1;
+ }
+ } else {
+ i += 1;
+ }
+ }
+ if seg_start < fmt.len {
+ code = concat(code, "result = concat(result, substr(fmt, ");
+ code = concat(code, int_to_string(seg_start));
+ code = concat(code, ", ");
+ code = concat(code, int_to_string(fmt.len - seg_start));
+ code = concat(code, ")); ");
+ }
+ code = concat(code, "result;");
+ code;
+}
+
+format :: ($fmt: string, args: ..Any) -> string {
+ #insert build_format(fmt);
+}
+
print :: ($fmt: string, args: ..Any) {
#insert build_print(fmt);
}
diff --git a/specs.md b/specs.md
index abf3da9..0f753c1 100644
--- a/specs.md
+++ b/specs.md
@@ -343,6 +343,27 @@ val := mp[2]; // 30
**Fat pointer layout**: `[:0]u8`, `string`, and `[]T` are `{ptr, i64}` structs. The raw pointer is always the first field at offset 0. This means `*[:0]u8` works as C's `char**` — a C function dereferences through the outer pointer and reads the raw `char*` from offset 0.
+### Foreign Function Interface (C Interop)
+
+To call C functions, declare a library constant with `#library` and bind functions with `#foreign`:
+
+```sx
+// Declare a named library constant
+libc :: #library "c";
+sdl :: #library "SDL3";
+
+// Bind foreign functions — library ref is required
+socket :: (domain: s32, type: s32, protocol: s32) -> s32 #foreign libc;
+SDL_Init :: (flags: u32) -> bool #foreign sdl;
+
+// Symbol renaming — optional second argument gives the C symbol name
+write_fd :: (fd: s32, buf: [*]u8, count: u64) -> s64 #foreign libc "write";
+```
+
+- `#library "name"` must be assigned to a named constant. The library is passed to the linker (`-lname` on Unix, `name.lib` on Windows).
+- `#foreign lib_ref` declares a function as external C. The library reference is mandatory.
+- `#foreign lib_ref "c_symbol"` renames the binding: the sx function name differs from the C symbol. This avoids name collisions (e.g. POSIX `write` vs an sx builtin).
+
### C Interop Type Mapping
| C type | sx type | Notes |
@@ -906,7 +927,7 @@ A variable declaration (`name :=`) inside an inner scope shadows any variable wi
Built-in functions are declared in `std.sx` with the `#builtin` suffix, which tells the compiler to generate the implementation internally rather than looking for a function body.
### I/O
-- `write(str: string) -> void` — write a string to standard output
+- `out(str: string) -> void` — write a string to standard output
- `print(fmt: string, args: ..Any)` — formatted print. Parses `{}` placeholders in the format string and substitutes arguments. When all argument types are statically known, the compiler specializes the call at compile time (no `Any` boxing).
### Math
diff --git a/src/ast.zig b/src/ast.zig
index bd42a71..682b990 100644
--- a/src/ast.zig
+++ b/src/ast.zig
@@ -61,7 +61,7 @@ pub const Node = struct {
continue_expr: void,
undef_literal: void,
builtin_expr: void,
- foreign_expr: void,
+ foreign_expr: ForeignExpr,
library_decl: LibraryDecl,
function_type_expr: FunctionTypeExpr,
@@ -368,8 +368,14 @@ pub const NamespaceDecl = struct {
decls: []const *Node,
};
+pub const ForeignExpr = struct {
+ library_ref: ?[]const u8 = null, // identifier name of library constant
+ c_name: ?[]const u8 = null, // C symbol name override
+};
+
pub const LibraryDecl = struct {
lib_name: []const u8,
+ name: []const u8, // sx-side constant name
};
pub const FunctionTypeExpr = struct {
diff --git a/src/codegen.zig b/src/codegen.zig
index 1e33d97..b8716f9 100644
--- a/src/codegen.zig
+++ b/src/codegen.zig
@@ -186,6 +186,10 @@ pub const CodeGen = struct {
foreign_libraries: std.ArrayList([]const u8),
// Set of foreign function names (for ABI lowering at call sites)
foreign_fns: std.StringHashMap(void),
+ // Named library constants: sx name → lib filename (e.g. "libc" → "c")
+ library_constants: std.StringHashMap([]const u8),
+ // Foreign function rename map: sx name → C symbol name (e.g. "write_fd" → "write")
+ foreign_name_map: std.StringHashMap([]const u8),
// Global mutable variables (from top-level var_decl, e.g. function pointers loaded at runtime)
global_mutable_vars: std.StringHashMap(NamedValue),
// Declared return types for non-generic functions (preserves signedness lost by LLVM round-trip)
@@ -394,6 +398,8 @@ pub const CodeGen = struct {
.deferred_fn_bodies = std.ArrayList(DeferredFn).empty,
.foreign_libraries = std.ArrayList([]const u8).empty,
.foreign_fns = std.StringHashMap(void).init(allocator),
+ .library_constants = std.StringHashMap([]const u8).init(allocator),
+ .foreign_name_map = std.StringHashMap([]const u8).init(allocator),
.global_mutable_vars = std.StringHashMap(NamedValue).init(allocator),
.function_return_types = std.StringHashMap(Type).init(allocator),
.target_config = target_config,
@@ -426,6 +432,8 @@ pub const CodeGen = struct {
self.deferred_fn_bodies.deinit(self.allocator);
self.foreign_libraries.deinit(self.allocator);
self.foreign_fns.deinit();
+ self.library_constants.deinit();
+ self.foreign_name_map.deinit();
c.LLVMDisposeBuilder(self.builder);
if (self.target_machine) |tm| c.LLVMDisposeTargetMachine(tm);
if (self.module_owned) {
@@ -1038,6 +1046,27 @@ pub const CodeGen = struct {
// Initialize built-in function declarations (printf, etc.)
self.builtins = Builtins.init(self.module, self.context);
+ // Pre-scan: collect named library constants (handles forward references)
+ for (root.data.root.decls) |decl| {
+ switch (decl.data) {
+ .library_decl => |ld| {
+ try self.library_constants.put(ld.name, ld.lib_name);
+ },
+ .namespace_decl => |ns| {
+ for (ns.decls) |nd| {
+ switch (nd.data) {
+ .library_decl => |nld| {
+ const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns.name, nld.name });
+ try self.library_constants.put(qualified, nld.lib_name);
+ },
+ else => {},
+ }
+ }
+ },
+ else => {},
+ }
+ }
+
// Pass 1: Register all declarations (signatures only, no bodies)
for (root.data.root.decls) |decl| {
switch (decl.data) {
@@ -1904,11 +1933,34 @@ pub const CodeGen = struct {
fn registerFnDecl(self: *CodeGen, fd: ast.FnDecl, llvm_name: []const u8) !void {
const is_foreign = fd.body.data == .foreign_expr;
+ // For foreign functions: resolve C symbol name (rename) and validate library ref
+ const actual_llvm_name = if (is_foreign) blk: {
+ const fe = fd.body.data.foreign_expr;
+ // Validate library reference
+ if (fe.library_ref) |lib_ref| {
+ if (!self.library_constants.contains(lib_ref)) {
+ return self.emitErrorFmt("unknown library '{s}' in #foreign", .{lib_ref});
+ }
+ }
+ // Use C symbol name if provided, otherwise use the sx name
+ const c_name = fe.c_name orelse llvm_name;
+ // Track rename mapping for call resolution
+ if (fe.c_name != null and !std.mem.eql(u8, c_name, llvm_name)) {
+ try self.foreign_name_map.put(llvm_name, c_name);
+ }
+ break :blk c_name;
+ } else llvm_name;
const fn_type = try self.buildFnType(fd.params, fd.return_type, fd.name, is_foreign);
- const name_z = try self.allocator.dupeZ(u8, llvm_name);
+ const name_z = try self.allocator.dupeZ(u8, actual_llvm_name);
_ = c.LLVMAddFunction(self.module, name_z.ptr, fn_type);
- // Track foreign functions for ABI lowering at call sites
- if (is_foreign) try self.foreign_fns.put(llvm_name, {});
+ // Track foreign functions for ABI lowering at call sites (use sx name for call-site lookup)
+ if (is_foreign) {
+ try self.foreign_fns.put(llvm_name, {});
+ // Also track under the C name for direct lookups
+ if (!std.mem.eql(u8, actual_llvm_name, llvm_name)) {
+ try self.foreign_fns.put(actual_llvm_name, {});
+ }
+ }
// Track resolved parameter types for accurate call-site conversion
var param_types = std.ArrayList(Type).empty;
for (fd.params) |param| {
@@ -1949,10 +2001,17 @@ pub const CodeGen = struct {
}
const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns.name, fd.name });
if (fd.body.data == .foreign_expr) {
- // External C function in namespace — register LLVM declaration with C name only
+ // External C function in namespace — register LLVM declaration
try self.registerFnDecl(fd, fd.name);
// Also track qualified name as foreign for ABI lowering at call sites
try self.foreign_fns.put(qualified, {});
+ // Track qualified rename mapping if C name differs
+ const fe = fd.body.data.foreign_expr;
+ if (fe.c_name) |c_name| {
+ if (!std.mem.eql(u8, c_name, fd.name)) {
+ try self.foreign_name_map.put(qualified, c_name);
+ }
+ }
// Store param types under qualified name so call-site type resolution works
var param_types = std.ArrayList(Type).empty;
for (fd.params) |param| {
@@ -2054,8 +2113,16 @@ pub const CodeGen = struct {
if (expr.data == .call) {
if (self.resolveCalleeName(expr.data.call)) |callee_name| {
var cnbuf: [256]u8 = undefined;
- const callee_fn = c.LLVMGetNamedFunction(self.module, self.nameToCStr(callee_name, &cnbuf)) orelse return Type.s(64);
- const fn_type = c.LLVMGlobalGetValueType(callee_fn);
+ var callee_fn = c.LLVMGetNamedFunction(self.module, self.nameToCStr(callee_name, &cnbuf));
+ // Foreign rename fallback
+ if (callee_fn == null) {
+ if (self.foreign_name_map.get(callee_name)) |c_name| {
+ var rbuf: [256]u8 = undefined;
+ callee_fn = c.LLVMGetNamedFunction(self.module, self.nameToCStr(c_name, &rbuf));
+ }
+ }
+ const resolved_fn = callee_fn orelse return Type.s(64);
+ const fn_type = c.LLVMGlobalGetValueType(resolved_fn);
const ret_llvm = c.LLVMGetReturnType(fn_type);
return self.llvmTypeToSxType(ret_llvm);
}
@@ -3160,6 +3227,13 @@ pub const CodeGen = struct {
fn_val = c.LLVMGetNamedFunction(self.module, self.nameToCStr(qualified, &qbuf));
}
}
+ // Foreign rename fallback: sx name → C symbol name
+ if (fn_val == null) {
+ if (self.foreign_name_map.get(ident.name)) |c_name| {
+ var rbuf: [256]u8 = undefined;
+ fn_val = c.LLVMGetNamedFunction(self.module, self.nameToCStr(c_name, &rbuf));
+ }
+ }
if (fn_val != null) return fn_val.?;
}
return self.emitErrorFmt("undefined identifier '{s}'", .{ident.name});
@@ -5386,6 +5460,13 @@ pub const CodeGen = struct {
callee_fn = c.LLVMGetNamedFunction(self.module, self.nameToCStr(qualified2, &qbuf));
}
}
+ // Foreign rename fallback: sx name → C symbol name (e.g. "write_fd" → "write")
+ if (callee_fn == null) {
+ if (self.foreign_name_map.get(callee_name)) |c_name| {
+ var rbuf: [256]u8 = undefined;
+ callee_fn = c.LLVMGetNamedFunction(self.module, self.nameToCStr(c_name, &rbuf));
+ }
+ }
// Function pointer indirect call: callee is a variable with function_type
if (callee_fn == null) {
if (self.lookupValue(callee_name)) |v| {
@@ -6795,7 +6876,7 @@ pub const CodeGen = struct {
fn dispatchBuiltin(self: *CodeGen, name: []const u8, call_node: ast.Call) !c.LLVMValueRef {
// Extract base name (strip namespace prefix)
const base = baseName(name);
- if (std.mem.eql(u8, base, "write")) return self.genWriteCall(call_node.args);
+ if (std.mem.eql(u8, base, "out")) return self.genOutCall(call_node.args);
if (std.mem.eql(u8, base, "sqrt")) return self.genMathIntrinsic(call_node, "sqrt");
if (std.mem.eql(u8, base, "sin")) return self.genMathIntrinsic(call_node, "sin");
if (std.mem.eql(u8, base, "cos")) return self.genMathIntrinsic(call_node, "cos");
@@ -6816,8 +6897,8 @@ pub const CodeGen = struct {
return self.emitErrorFmt("unknown builtin function '{s}'", .{name});
}
- fn genWriteCall(self: *CodeGen, args: []const *Node) !c.LLVMValueRef {
- if (args.len != 1) return self.emitError("write expects exactly 1 argument");
+ fn genOutCall(self: *CodeGen, args: []const *Node) !c.LLVMValueRef {
+ if (args.len != 1) return self.emitError("out expects exactly 1 argument");
const builtins = try self.requireBuiltins();
const val = try self.genExpr(args[0]);
// Extract ptr and len from string slice
@@ -7311,7 +7392,7 @@ pub const CodeGen = struct {
}
defer _ = c.LLVMOrcDisposeLLJIT(jit);
- // Add process symbols so JIT can find libc (printf, write, etc.)
+ // Add process symbols so JIT can find libc (printf, etc.)
const jd = c.LLVMOrcLLJITGetMainJITDylib(jit);
const prefix = c.LLVMOrcLLJITGetGlobalPrefix(jit);
var gen: c.LLVMOrcDefinitionGeneratorRef = null;
diff --git a/src/comptime.zig b/src/comptime.zig
index bb18d7a..48dc6df 100644
--- a/src/comptime.zig
+++ b/src/comptime.zig
@@ -217,7 +217,7 @@ pub const Instruction = union(enum) {
pub const ValueKind = enum { int, float, f32_k, bool_k, string };
-pub const BuiltinId = enum { print, write, sqrt, size_of, cast, alloc };
+pub const BuiltinId = enum { print, out, sqrt, size_of, cast, alloc };
/// A compiled function or expression — a flat sequence of instructions.
pub const Chunk = struct {
@@ -1354,8 +1354,8 @@ pub const VM = struct {
fn callBuiltin(self: *VM, id: BuiltinId, arg_count: u8) !void {
switch (id) {
- .write => {
- // write(str) — raw string output
+ .out => {
+ // out(str) — raw string output
if (arg_count >= 1) {
const val = try self.pop();
const str = try val.format(self.allocator);
diff --git a/src/lsp/server.zig b/src/lsp/server.zig
index 9dadc12..f35f204 100644
--- a/src/lsp/server.zig
+++ b/src/lsp/server.zig
@@ -879,7 +879,7 @@ pub const Server = struct {
.{ .name = "alloc", .label = "alloc(size: s32) -> string", .params = &.{"size: s32"} },
.{ .name = "sqrt", .label = "sqrt(x: $T) -> T", .params = &.{"x: $T"} },
.{ .name = "print", .label = "print(fmt: string, args: ..Any)", .params = &.{ "fmt: string", "args: ..Any" } },
- .{ .name = "write", .label = "write(str: string) -> void", .params = &.{"str: string"} },
+ .{ .name = "out", .label = "out(str: string) -> void", .params = &.{"str: string"} },
};
for (&builtin_sigs) |b| {
const matches = std.mem.eql(u8, call_ctx.name, b.name) or
diff --git a/src/parser.zig b/src/parser.zig
index 27f9510..0663784 100644
--- a/src/parser.zig
+++ b/src/parser.zig
@@ -62,19 +62,6 @@ pub const Parser = struct {
return try self.createNode(start, .{ .import_decl = .{ .path = path, .name = null } });
}
- // Top-level #library directive: #library "libname";
- if (self.current.tag == .hash_library) {
- self.advance();
- if (self.current.tag != .string_literal) {
- return self.fail("expected string after '#library'");
- }
- const raw = self.tokenSlice(self.current);
- const lib_name = raw[1 .. raw.len - 1];
- self.advance();
- try self.expect(.semicolon);
- return try self.createNode(start, .{ .library_decl = .{ .lib_name = lib_name } });
- }
-
// Top-level #run directive
if (self.current.tag == .hash_run) {
self.advance();
@@ -131,6 +118,19 @@ pub const Parser = struct {
return try self.createNode(start_pos, .{ .import_decl = .{ .path = path, .name = name } });
}
+ // Named library: name :: #library "libname";
+ if (self.current.tag == .hash_library) {
+ self.advance();
+ if (self.current.tag != .string_literal) {
+ return self.fail("expected string after '#library'");
+ }
+ const raw = self.tokenSlice(self.current);
+ const lib_name = raw[1 .. raw.len - 1];
+ self.advance();
+ try self.expect(.semicolon);
+ return try self.createNode(start_pos, .{ .library_decl = .{ .lib_name = lib_name, .name = name } });
+ }
+
// Compile-time evaluation: name :: #run expr;
if (self.current.tag == .hash_run) {
const run_start = self.current.loc.start;
@@ -192,12 +192,27 @@ pub const Parser = struct {
return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = value, .value = bi } });
}
- // name :: type_expr #foreign; — foreign with type annotation
+ // name :: type_expr #foreign lib ["c_name"]; — foreign with type annotation
if (self.current.tag == .hash_foreign) {
const fi_start = self.current.loc.start;
self.advance();
+ // Required: library reference (identifier)
+ if (self.current.tag != .identifier)
+ return self.fail("expected library name after '#foreign'");
+ const lib_ref = self.tokenSlice(self.current);
+ self.advance();
+ // Optional: C symbol name (string literal)
+ var c_name: ?[]const u8 = null;
+ if (self.current.tag == .string_literal) {
+ const raw = self.tokenSlice(self.current);
+ c_name = raw[1 .. raw.len - 1];
+ self.advance();
+ }
try self.expect(.semicolon);
- const fi = try self.createNode(fi_start, .{ .foreign_expr = {} });
+ const fi = try self.createNode(fi_start, .{ .foreign_expr = .{
+ .library_ref = lib_ref,
+ .c_name = c_name,
+ } });
return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = value, .value = fi } });
}
@@ -754,8 +769,23 @@ pub const Parser = struct {
} else if (self.current.tag == .hash_foreign) blk: {
const fi_start = self.current.loc.start;
self.advance();
+ // Required: library reference (identifier)
+ if (self.current.tag != .identifier)
+ return self.fail("expected library name after '#foreign'");
+ const lib_ref = self.tokenSlice(self.current);
+ self.advance();
+ // Optional: C symbol name (string literal)
+ var c_name: ?[]const u8 = null;
+ if (self.current.tag == .string_literal) {
+ const raw = self.tokenSlice(self.current);
+ c_name = raw[1 .. raw.len - 1];
+ self.advance();
+ }
try self.expect(.semicolon);
- break :blk try self.createNode(fi_start, .{ .foreign_expr = {} });
+ break :blk try self.createNode(fi_start, .{ .foreign_expr = .{
+ .library_ref = lib_ref,
+ .c_name = c_name,
+ } });
} else if (self.current.tag == .fat_arrow) blk: {
is_arrow = true;
self.advance();
@@ -1826,7 +1856,7 @@ test "parse namespaced import" {
}
test "parse library declaration" {
- const source = "#library \"raylib\";";
+ const source = "rl :: #library \"raylib\";";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
@@ -1835,6 +1865,7 @@ test "parse library declaration" {
const decl = root.data.root.decls[0];
try std.testing.expect(decl.data == .library_decl);
try std.testing.expectEqualStrings("raylib", decl.data.library_decl.lib_name);
+ try std.testing.expectEqualStrings("rl", decl.data.library_decl.name);
}
test "parse void function with builtin body" {
@@ -1851,7 +1882,7 @@ test "parse void function with builtin body" {
}
test "parse void function with foreign body" {
- const source = "InitWindow :: (width: s32, height: s32, title: *u8) -> void #foreign;";
+ const source = "InitWindow :: (width: s32, height: s32, title: *u8) -> void #foreign rl;";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
@@ -1861,6 +1892,7 @@ test "parse void function with foreign body" {
try std.testing.expect(decl.data == .fn_decl);
try std.testing.expectEqualStrings("InitWindow", decl.data.fn_decl.name);
try std.testing.expect(decl.data.fn_decl.body.data == .foreign_expr);
+ try std.testing.expectEqualStrings("rl", decl.data.fn_decl.body.data.foreign_expr.library_ref.?);
try std.testing.expectEqual(@as(usize, 3), decl.data.fn_decl.params.len);
}
diff --git a/tests/expected/50-smoke.txt b/tests/expected/50-smoke.txt
index dd5ee13..0b1b62e 100644
--- a/tests/expected/50-smoke.txt
+++ b/tests/expected/50-smoke.txt
@@ -195,7 +195,7 @@ outer-defer
defer-in-if: body
defer-in-if: deferred
=== 7. Builtins ===
-write-ok
+out-ok
sqrt: 3.000000
sqrt-f64: 4.000000
sizeof-s32: 4
@@ -240,4 +240,6 @@ flags-explicit-raw: 68
var swap: 20 10
arr swap: 3 1
3-way: 3 1 2
+=== 15. Foreign ===
+foreign-rename: 42
=== DONE ===