From 9d96f05d3b7c78104582da08af265354068c827f Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 11 Feb 2026 20:41:43 +0200 Subject: [PATCH] graphics --- examples/24-list.sx | 23 +-- examples/27-graphics.sx | 19 +++ examples/modules/raylib.sx | 17 +++ examples/modules/std.sx | 21 +++ src/ast.zig | 6 + src/codegen.zig | 302 +++++++++++++++++++++++++++++++------ src/lexer.zig | 23 ++- src/lsp/server.zig | 2 + src/main.zig | 2 +- src/parser.zig | 76 +++++++++- src/sema.zig | 4 + src/token.zig | 2 + src/types.zig | 33 ++++ 13 files changed, 460 insertions(+), 70 deletions(-) create mode 100644 examples/27-graphics.sx create mode 100644 examples/modules/raylib.sx diff --git a/examples/24-list.sx b/examples/24-list.sx index 4dad4ac..350f4be 100644 --- a/examples/24-list.sx +++ b/examples/24-list.sx @@ -1,29 +1,8 @@ #import "modules/std.sx"; -List :: struct ($T: Type) { - items: [*]T = null; - len: s64 = 0; - cap: s64 = 0; -} - -append :: (list: *List($T), item: T) { - if list.len >= list.cap { - new_cap := if list.cap == 0 then 4 else list.cap * 2; - new_items : [*]T = xx malloc(new_cap * size_of(T)); - if list.len > 0 { - memcpy(new_items, list.items, list.len * size_of(T)); - free(list.items); - } - list.items = new_items; - list.cap = new_cap; - } - list.items[list.len] = item; - list.len += 1; -} - main :: () { list := List(s32).{}; - append(list, 3); + append(list, 1); list.append(3); diff --git a/examples/27-graphics.sx b/examples/27-graphics.sx new file mode 100644 index 0000000..35a08c8 --- /dev/null +++ b/examples/27-graphics.sx @@ -0,0 +1,19 @@ +#import "modules/raylib.sx"; + +main :: () { + InitWindow(800, 600, "sx - Triangle"); + + while !WindowShouldClose() { + BeginDrawing(); + ClearBackground(Color.{50, 50, 50, 255}); + + v1 := Vector2.{400.0, 100.0}; + v2 := Vector2.{200.0, 500.0}; + v3 := Vector2.{600.0, 500.0}; + DrawTriangle(v1, v2, v3, Color.{230, 41, 55, 255}); + + EndDrawing(); + } + + CloseWindow(); +} diff --git a/examples/modules/raylib.sx b/examples/modules/raylib.sx new file mode 100644 index 0000000..3cc838c --- /dev/null +++ b/examples/modules/raylib.sx @@ -0,0 +1,17 @@ +#library "raylib"; + +Color :: struct { + r, g, b, a: u8; +} + +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; diff --git a/examples/modules/std.sx b/examples/modules/std.sx index 2b269e0..a1c2883 100644 --- a/examples/modules/std.sx +++ b/examples/modules/std.sx @@ -305,3 +305,24 @@ build_print :: (fmt: string) -> string { print :: ($fmt: string, args: ..Any) { #insert build_print(fmt); } + +List :: struct ($T: Type) { + items: [*]T = null; + len: s64 = 0; + cap: s64 = 0; +} + +append :: (list: *List($T), item: T) { + if list.len >= list.cap { + new_cap := if list.cap == 0 then 4 else list.cap * 2; + new_items : [*]T = xx malloc(new_cap * size_of(T)); + if list.len > 0 { + memcpy(new_items, list.items, list.len * size_of(T)); + free(list.items); + } + list.items = new_items; + list.cap = new_cap; + } + list.items[list.len] = item; + list.len += 1; +} \ No newline at end of file diff --git a/src/ast.zig b/src/ast.zig index 90f1451..3f068bf 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -61,6 +61,8 @@ pub const Node = struct { continue_expr: void, undef_literal: void, builtin_expr: void, + foreign_expr: void, + library_decl: LibraryDecl, pub fn declName(self: Data) ?[]const u8 { return switch (self) { @@ -353,3 +355,7 @@ pub const NamespaceDecl = struct { name: []const u8, decls: []const *Node, }; + +pub const LibraryDecl = struct { + lib_name: []const u8, +}; diff --git a/src/codegen.zig b/src/codegen.zig index 85bf548..40cda46 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -91,6 +91,10 @@ pub const CodeGen = struct { current_match_tags: ?[]const u64 = null, // Functions deferred to compile after all types are registered (e.g. any_to_string) deferred_fn_bodies: std.ArrayList(DeferredFn), + // Libraries to link against (from #library directives) + foreign_libraries: std.ArrayList([]const u8), + // Set of foreign function names (for ABI lowering at call sites) + foreign_fns: std.StringHashMap(void), const DeferredFn = struct { fd: ast.FnDecl, @@ -194,6 +198,8 @@ pub const CodeGen = struct { .any_type_id_map = std.StringHashMap(u64).init(allocator), .any_type_entries = std.StringHashMap(AnyTypeEntry).init(allocator), .deferred_fn_bodies = std.ArrayList(DeferredFn).empty, + .foreign_libraries = std.ArrayList([]const u8).empty, + .foreign_fns = std.StringHashMap(void).init(allocator), }; } @@ -214,6 +220,8 @@ pub const CodeGen = struct { self.any_type_id_map.deinit(); self.any_type_entries.deinit(); self.deferred_fn_bodies.deinit(self.allocator); + self.foreign_libraries.deinit(self.allocator); + self.foreign_fns.deinit(); c.LLVMDisposeBuilder(self.builder); c.LLVMDisposeModule(self.module); c.LLVMContextDispose(self.context); @@ -229,6 +237,21 @@ pub const CodeGen = struct { return error.CodeGenError; } + /// Build an alloca in the entry block of the current function so that + /// stack space is reserved once, not on every loop iteration. + fn buildEntryBlockAlloca(self: *CodeGen, ty: c.LLVMTypeRef, name: [*:0]const u8) c.LLVMValueRef { + const entry_bb = c.LLVMGetEntryBasicBlock(self.current_function); + const first_instr = c.LLVMGetFirstInstruction(entry_bb); + const tmp_builder = c.LLVMCreateBuilderInContext(self.context); + defer c.LLVMDisposeBuilder(tmp_builder); + if (first_instr != null) { + c.LLVMPositionBuilderBefore(tmp_builder, first_instr); + } else { + c.LLVMPositionBuilderAtEnd(tmp_builder, entry_bb); + } + return c.LLVMBuildAlloca(tmp_builder, ty, name); + } + pub fn typeToLLVM(self: *CodeGen, ty: Type) c.LLVMTypeRef { return switch (ty) { .signed => |w| c.LLVMIntTypeInContext(self.context, w), @@ -347,11 +370,17 @@ pub const CodeGen = struct { /// Build an Any value { tag: i32, value: i64 } from a typed LLVM value. /// Small values (ints, floats, bools, enums) are stored inline in the i64. /// Complex values (strings, structs, unions) are stored via pointer (alloca + ptr-to-int). - fn buildAnyValue(self: *CodeGen, val: c.LLVMValueRef, ty: Type) !c.LLVMValueRef { + fn buildAnyValue(self: *CodeGen, val: c.LLVMValueRef, in_ty: Type) !c.LLVMValueRef { const any_ty = self.getAnyStructType(); const i64_ty = c.LLVMInt64TypeInContext(self.context); const undef = c.LLVMGetUndef(any_ty); + // []u8 boxes as string (same repr, same Any tag) + const ty: Type = if (in_ty.isSlice() and std.mem.eql(u8, in_ty.slice_type.element_name, "u8")) + .string_type + else + in_ty; + // Determine tag const tag: u64 = switch (ty) { .void_type => ANY_TAG_VOID, @@ -394,7 +423,7 @@ pub const CodeGen = struct { .f64 => c.LLVMBuildBitCast(self.builder, val, i64_ty, "any_f64"), .string_type => blk: { // String is {ptr, i32} — store to alloca, pass alloca as i64 - const str_alloca = c.LLVMBuildAlloca(self.builder, self.getStringStructType(), "any_str_tmp"); + const str_alloca = self.buildEntryBlockAlloca(self.getStringStructType(), "any_str_tmp"); _ = c.LLVMBuildStore(self.builder, val, str_alloca); break :blk c.LLVMBuildPtrToInt(self.builder, str_alloca, i64_ty, "any_str"); }, @@ -402,7 +431,7 @@ pub const CodeGen = struct { // Struct — store to alloca, pass pointer as i64 const info = self.struct_types.get(sname) orelse return c.LLVMGetUndef(any_ty); - const alloca = c.LLVMBuildAlloca(self.builder, info.llvm_type, "any_struct_tmp"); + const alloca = self.buildEntryBlockAlloca(info.llvm_type, "any_struct_tmp"); _ = c.LLVMBuildStore(self.builder, val, alloca); break :blk c.LLVMBuildPtrToInt(self.builder, alloca, i64_ty, "any_struct"); }, @@ -414,20 +443,20 @@ pub const CodeGen = struct { // Union — store to alloca, pass pointer as i64 const info = self.union_types.get(uname) orelse return c.LLVMGetUndef(any_ty); - const alloca = c.LLVMBuildAlloca(self.builder, info.llvm_type, "any_union_tmp"); + const alloca = self.buildEntryBlockAlloca(info.llvm_type, "any_union_tmp"); _ = c.LLVMBuildStore(self.builder, val, alloca); break :blk c.LLVMBuildPtrToInt(self.builder, alloca, i64_ty, "any_union"); }, .vector_type, .array_type => blk: { // Vector/Array — store to alloca, pass pointer as i64 const llvm_ty = self.typeToLLVM(ty); - const alloca = c.LLVMBuildAlloca(self.builder, llvm_ty, "any_vec_tmp"); + const alloca = self.buildEntryBlockAlloca(llvm_ty, "any_vec_tmp"); _ = c.LLVMBuildStore(self.builder, val, alloca); break :blk c.LLVMBuildPtrToInt(self.builder, alloca, i64_ty, "any_vec"); }, .slice_type => blk: { // Slice {ptr, i32} — store to alloca, pass pointer as i64 - const alloca = c.LLVMBuildAlloca(self.builder, self.getStringStructType(), "any_slice_tmp"); + const alloca = self.buildEntryBlockAlloca(self.getStringStructType(), "any_slice_tmp"); _ = c.LLVMBuildStore(self.builder, val, alloca); break :blk c.LLVMBuildPtrToInt(self.builder, alloca, i64_ty, "any_slice"); }, @@ -435,7 +464,7 @@ pub const CodeGen = struct { .meta_type => |mt| blk: { // Meta type: wrap raw char ptr in string slice {ptr, len} for extraction const str_slice = self.buildStringSlice(val, mt.name.len); - const alloca = c.LLVMBuildAlloca(self.builder, self.getStringStructType(), "any_type_tmp"); + const alloca = self.buildEntryBlockAlloca(self.getStringStructType(), "any_type_tmp"); _ = c.LLVMBuildStore(self.builder, str_slice, alloca); break :blk c.LLVMBuildPtrToInt(self.builder, alloca, i64_ty, "any_type"); }, @@ -542,6 +571,9 @@ pub const CodeGen = struct { .fn_decl => |fd| { if (fd.body.data == .builtin_expr) { try self.builtin_functions.put(fd.name, {}); + } else if (fd.body.data == .foreign_expr) { + // External C function — register LLVM declaration (no body) + try self.registerFnDecl(fd); } else if (fd.type_params.len > 0) { try self.generic_templates.put(fd.name, .{ .fd = fd }); } else { @@ -549,6 +581,9 @@ pub const CodeGen = struct { } try self.fn_signatures.put(fd.name, self.buildFnSignature(fd)); }, + .library_decl => |ld| { + try self.foreign_libraries.append(self.allocator, ld.lib_name); + }, .enum_decl => |ed| { try self.enum_types.put(ed.name, ed.variants); _ = try self.getAnyTypeId(ed.name, .{ .enum_type = ed.name }); @@ -642,8 +677,8 @@ pub const CodeGen = struct { for (root.data.root.decls) |decl| { switch (decl.data) { .fn_decl => |fd| { - if (fd.body.data == .builtin_expr) { - // skip + if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr) { + // skip — no body to generate } else if (fd.type_params.len == 0) { if (shouldDeferFnBody(fd)) { try self.deferred_fn_bodies.append(self.allocator, .{ .fd = fd, .name = fd.name }); @@ -1100,6 +1135,10 @@ pub const CodeGen = struct { } fn buildFnType(self: *CodeGen, params: []const ast.Param, return_type: ?*Node, name: []const u8) !c.LLVMTypeRef { + return self.buildFnTypeEx(params, return_type, name, false); + } + + fn buildFnTypeEx(self: *CodeGen, params: []const ast.Param, return_type: ?*Node, name: []const u8, is_foreign: bool) !c.LLVMTypeRef { const ret_sx_type = self.resolveType(return_type); const is_main = std.mem.eql(u8, name, "main"); const ret_llvm_type = if (is_main) @@ -1116,7 +1155,14 @@ pub const CodeGen = struct { } else { const sx_ty = self.resolveType(param.type_expr); if (sx_ty == .void_type) return self.emitErrorFmt("parameter '{s}' in function '{s}' has unresolved type", .{ param.name, name }); - try param_llvm_types.append(self.allocator, self.typeToLLVM(sx_ty)); + // Foreign functions: apply C ABI lowering + if (is_foreign and sx_ty == .string_type) { + try param_llvm_types.append(self.allocator, c.LLVMPointerTypeInContext(self.context, 0)); + } else if (is_foreign and sx_ty.isStruct()) { + try param_llvm_types.append(self.allocator, self.getForeignParamABIType(sx_ty)); + } else { + try param_llvm_types.append(self.allocator, self.typeToLLVM(sx_ty)); + } } } const params_slice = try param_llvm_types.toOwnedSlice(self.allocator); @@ -1129,14 +1175,82 @@ pub const CodeGen = struct { ); } + /// For foreign (C ABI) functions on ARM64, struct parameters must be lowered + /// to their ABI-equivalent types. LLVM does NOT do this automatically. + /// - HFA (1-4 same float/double fields): [N x float/double] + /// - Non-HFA ≤ 8 bytes: i64 + /// - Non-HFA 9-16 bytes: [2 x i64] + fn getForeignParamABIType(self: *CodeGen, sx_ty: Type) c.LLVMTypeRef { + const is_aarch64 = comptime @import("builtin").cpu.arch == .aarch64; + if (!is_aarch64) return self.typeToLLVM(sx_ty); + if (!sx_ty.isStruct()) return self.typeToLLVM(sx_ty); + + const sname = self.type_aliases.get(sx_ty.struct_type) orelse sx_ty.struct_type; + const info = self.struct_types.get(sname) orelse return self.typeToLLVM(sx_ty); + + // Check HFA: 1-4 fields all of the same float type + const field_types = info.field_types; + if (field_types.len >= 1 and field_types.len <= 4) { + const first = field_types[0]; + if (first == .f32 or first == .f64) { + var all_same = true; + for (field_types[1..]) |ft| { + if (!std.meta.eql(ft, first)) { + all_same = false; + break; + } + } + if (all_same) { + const elem_ty = if (first == .f32) + c.LLVMFloatTypeInContext(self.context) + else + c.LLVMDoubleTypeInContext(self.context); + return c.LLVMArrayType2(elem_ty, @intCast(field_types.len)); + } + } + } + + // Non-HFA: pack into integer registers + const data_layout = c.LLVMGetModuleDataLayout(self.module); + const size = c.LLVMStoreSizeOfType(data_layout, info.llvm_type); + if (size <= 8) return c.LLVMInt64TypeInContext(self.context); + if (size <= 16) return c.LLVMArrayType2(c.LLVMInt64TypeInContext(self.context), 2); + // > 16 bytes: pass by pointer (indirect) — not yet handled, fall back to struct type + return info.llvm_type; + } + + /// Convert a struct value to its C ABI representation for a foreign call. + /// Stores the struct to memory, then loads as the ABI type. + fn convertStructToABI(self: *CodeGen, struct_val: c.LLVMValueRef, struct_ty: c.LLVMTypeRef, abi_ty: c.LLVMTypeRef) c.LLVMValueRef { + const data_layout = c.LLVMGetModuleDataLayout(self.module); + const struct_size = c.LLVMStoreSizeOfType(data_layout, struct_ty); + const abi_size = c.LLVMStoreSizeOfType(data_layout, abi_ty); + + if (struct_size == abi_size) { + // Same size (e.g. {float, float} → [2 x float]): store and reload + const alloca = self.buildEntryBlockAlloca(struct_ty, "abi_tmp"); + _ = c.LLVMBuildStore(self.builder, struct_val, alloca); + return c.LLVMBuildLoad2(self.builder, abi_ty, alloca, "abi_arg"); + } else { + // Struct smaller than ABI type (e.g. {i8,i8,i8,i8} → i64): zero-init, then store struct + const alloca = self.buildEntryBlockAlloca(abi_ty, "abi_tmp"); + _ = c.LLVMBuildStore(self.builder, c.LLVMConstNull(abi_ty), alloca); + _ = c.LLVMBuildStore(self.builder, struct_val, alloca); + return c.LLVMBuildLoad2(self.builder, abi_ty, alloca, "abi_arg"); + } + } + fn registerFnDecl(self: *CodeGen, fd: ast.FnDecl) !void { return self.registerFnDeclAs(fd, fd.name); } fn registerFnDeclAs(self: *CodeGen, fd: ast.FnDecl, llvm_name: []const u8) !void { - const fn_type = try self.buildFnType(fd.params, fd.return_type, fd.name); + const is_foreign = fd.body.data == .foreign_expr; + const fn_type = try self.buildFnTypeEx(fd.params, fd.return_type, fd.name, is_foreign); const name_z = try self.allocator.dupeZ(u8, 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 resolved parameter types for accurate call-site conversion var param_types = std.ArrayList(Type).empty; for (fd.params) |param| { @@ -1173,7 +1287,19 @@ pub const CodeGen = struct { continue; } const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns.name, fd.name }); - if (fd.type_params.len > 0) { + if (fd.body.data == .foreign_expr) { + // External C function in namespace — register LLVM declaration with C name only + try self.registerFnDeclAs(fd, fd.name); + // Also track qualified name as foreign for ABI lowering at call sites + try self.foreign_fns.put(qualified, {}); + // Store param types under qualified name so call-site type resolution works + var param_types = std.ArrayList(Type).empty; + for (fd.params) |param| { + if (param.is_comptime) continue; + try param_types.append(self.allocator, self.resolveType(param.type_expr)); + } + try self.fn_param_types.put(qualified, try param_types.toOwnedSlice(self.allocator)); + } else if (fd.type_params.len > 0) { try self.generic_templates.put(qualified, .{ .fd = fd }); } else { try self.registerFnDeclAs(fd, qualified); @@ -1183,8 +1309,17 @@ pub const CodeGen = struct { const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns.name, ed.name }); try self.enum_types.put(qualified, ed.variants); }, - .struct_decl => |sd| try self.registerStructType(sd), - .union_decl => |ud| try self.registerUnionType(ud), + .struct_decl => |sd| { + try self.registerStructType(sd); + // Register qualified alias so rl.Color resolves to Color + const qualified_s = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns.name, sd.name }); + try self.type_aliases.put(qualified_s, sd.name); + }, + .union_decl => |ud| { + try self.registerUnionType(ud); + const qualified_u = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns.name, ud.name }); + try self.type_aliases.put(qualified_u, ud.name); + }, .const_decl => |cd| { if (cd.value.data == .builtin_expr) { // #builtin constant in namespace — skip codegen @@ -1196,6 +1331,9 @@ pub const CodeGen = struct { try self.type_aliases.put(qualified, cd.value.data.type_expr.name); } }, + .library_decl => |ld| { + try self.foreign_libraries.append(self.allocator, ld.lib_name); + }, else => {}, } } @@ -1209,8 +1347,8 @@ pub const CodeGen = struct { for (ns.decls) |decl| { switch (decl.data) { .fn_decl => |fd| { - if (fd.body.data == .builtin_expr) { - // skip + if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr) { + // skip — no body to generate } else if (fd.type_params.len == 0) { const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns.name, fd.name }); if (shouldDeferFnBody(fd)) { @@ -1636,7 +1774,7 @@ pub const CodeGen = struct { const type_name = try self.allocator.dupeZ(u8, raw_name); const name_z = try self.allocator.dupeZ(u8, vd.name); const ptr_ty = c.LLVMPointerTypeInContext(self.context, 0); - const alloca = c.LLVMBuildAlloca(self.builder, ptr_ty, name_z.ptr); + const alloca = self.buildEntryBlockAlloca(ptr_ty, name_z.ptr); const str_val = c.LLVMBuildGlobalStringPtr(self.builder, type_name.ptr, "type_name"); _ = c.LLVMBuildStore(self.builder, str_val, alloca); try self.saveShadowed(vd.name); @@ -1676,7 +1814,7 @@ pub const CodeGen = struct { sx_ty = .{ .struct_type = sname }; const info = self.struct_types.get(sname) orelse return self.emitErrorFmt("unknown struct type '{s}'", .{sname}); const name_z = try self.allocator.dupeZ(u8, vd.name); - const alloca = c.LLVMBuildAlloca(self.builder, info.llvm_type, name_z.ptr); + const alloca = self.buildEntryBlockAlloca(info.llvm_type, name_z.ptr); if (vd.value == null) { // Default-init: per-field defaults or zero @@ -1711,7 +1849,7 @@ pub const CodeGen = struct { sx_ty = .{ .union_type = uname }; const info = self.union_types.get(uname) orelse return self.emitErrorFmt("unknown union type '{s}'", .{uname}); const name_z = try self.allocator.dupeZ(u8, vd.name); - const alloca = c.LLVMBuildAlloca(self.builder, info.llvm_type, name_z.ptr); + const alloca = self.buildEntryBlockAlloca(info.llvm_type, name_z.ptr); if (vd.value == null) { // Zero-init: tag=0, payload zeroed @@ -1756,7 +1894,7 @@ pub const CodeGen = struct { const arr_info = sx_ty.array_type; const llvm_arr_ty = self.typeToLLVM(sx_ty); const arr_name_z = try self.allocator.dupeZ(u8, vd.name); - const arr_alloca = c.LLVMBuildAlloca(self.builder, llvm_arr_ty, arr_name_z.ptr); + const arr_alloca = self.buildEntryBlockAlloca(llvm_arr_ty, arr_name_z.ptr); if (vd.value == null) { _ = c.LLVMBuildStore(self.builder, c.LLVMConstNull(llvm_arr_ty), arr_alloca); @@ -1798,7 +1936,7 @@ pub const CodeGen = struct { if (sx_ty.isVector()) { const llvm_vec_ty = self.typeToLLVM(sx_ty); const vec_name_z = try self.allocator.dupeZ(u8, vd.name); - const vec_alloca = c.LLVMBuildAlloca(self.builder, llvm_vec_ty, vec_name_z.ptr); + const vec_alloca = self.buildEntryBlockAlloca(llvm_vec_ty, vec_name_z.ptr); if (vd.value == null) { _ = c.LLVMBuildStore(self.builder, c.LLVMConstNull(llvm_vec_ty), vec_alloca); @@ -1826,7 +1964,7 @@ pub const CodeGen = struct { // Non-struct types const llvm_ty = self.typeToLLVM(sx_ty); const name_z = try self.allocator.dupeZ(u8, vd.name); - const alloca = c.LLVMBuildAlloca(self.builder, llvm_ty, name_z.ptr); + const alloca = self.buildEntryBlockAlloca(llvm_ty, name_z.ptr); if (vd.value == null) { // Default-init: zero @@ -1912,7 +2050,7 @@ pub const CodeGen = struct { const llvm_ty = self.typeToLLVM(sx_ty); const name_z = try self.allocator.dupeZ(u8, cd.name); - const alloca = c.LLVMBuildAlloca(self.builder, llvm_ty, name_z.ptr); + const alloca = self.buildEntryBlockAlloca(llvm_ty, name_z.ptr); _ = c.LLVMBuildStore(self.builder, init_val, alloca); try self.saveShadowed(cd.name); try self.named_values.put(cd.name, .{ .ptr = alloca, .ty = sx_ty }); @@ -2571,7 +2709,7 @@ pub const CodeGen = struct { const idx = variant_idx orelse return self.emitErrorFmt("no variant '{s}' in union '{s}'", .{ ul.variant_name, resolved_name }); // Alloca union - const alloca = c.LLVMBuildAlloca(self.builder, info.llvm_type, "union_tmp"); + const alloca = self.buildEntryBlockAlloca(info.llvm_type, "union_tmp"); const i64_ty = c.LLVMInt64TypeInContext(self.context); // Store tag (field 0) @@ -2613,7 +2751,7 @@ pub const CodeGen = struct { // Alloca the struct and default-init all fields (zero or declared defaults) const name_z = try self.allocator.dupeZ(u8, sname); - const alloca = c.LLVMBuildAlloca(self.builder, info.llvm_type, name_z.ptr); + const alloca = self.buildEntryBlockAlloca(info.llvm_type, name_z.ptr); try self.genStructDefaultInit(alloca, info); // Determine if this is named or positional mode @@ -2671,7 +2809,7 @@ pub const CodeGen = struct { const arr_info = arr_ty.array_type; const elem_sx_ty = Type.fromName(arr_info.element_name) orelse return self.emitErrorFmt("unknown array element type '{s}'", .{arr_info.element_name}); const llvm_arr_ty = self.typeToLLVM(arr_ty); - const alloca = c.LLVMBuildAlloca(self.builder, llvm_arr_ty, "arr"); + const alloca = self.buildEntryBlockAlloca(llvm_arr_ty, "arr"); const len = @min(al.elements.len, arr_info.length); for (0..len) |i| { @@ -2694,7 +2832,7 @@ pub const CodeGen = struct { // Create backing array [N]elem on the stack const arr_ty: Type = .{ .array_type = .{ .element_name = elem_name, .length = n } }; const llvm_arr_ty = self.typeToLLVM(arr_ty); - const arr_alloca = c.LLVMBuildAlloca(self.builder, llvm_arr_ty, "slice_backing"); + const arr_alloca = self.buildEntryBlockAlloca(llvm_arr_ty, "slice_backing"); // Fill elements for (0..n) |i| { @@ -2758,6 +2896,13 @@ pub const CodeGen = struct { return self.convertValue(val, src_ty, target_ty); } + // String literal → pointer context: produce raw pointer directly (no {ptr, len} wrapping) + if (node.data == .string_literal and target_ty.isPointer()) { + const unescaped = try unescapeString(self.allocator, node.data.string_literal.raw); + const str_z = try self.allocator.dupeZ(u8, unescaped); + return c.LLVMBuildGlobalStringPtr(self.builder, str_z.ptr, "str"); + } + // Enum literal assigned to union type: construct tag-only (void variant) union if (node.data == .enum_literal and target_ty.isUnion()) { const ul = ast.UnionLiteral{ @@ -2799,7 +2944,7 @@ pub const CodeGen = struct { const variant_ty = info.variant_types[idx]; // Alloca union, store tag - const alloca = c.LLVMBuildAlloca(self.builder, info.llvm_type, "union_lit"); + const alloca = self.buildEntryBlockAlloca(info.llvm_type, "union_lit"); const i32_ty = c.LLVMInt32TypeInContext(self.context); const tag_gep = c.LLVMBuildStructGEP2(self.builder, info.llvm_type, alloca, 0, "tag"); _ = c.LLVMBuildStore(self.builder, c.LLVMConstInt(i32_ty, idx, 0), tag_gep); @@ -2826,7 +2971,13 @@ pub const CodeGen = struct { // Struct literal targeting struct type: pass struct name context if (node.data == .struct_literal and target_ty.isStruct()) { - return self.genStructLiteral(node.data.struct_literal, target_ty.struct_type); + const alloca = try self.genStructLiteral(node.data.struct_literal, target_ty.struct_type); + // genStructLiteral returns an alloca pointer — load the value for by-value passing + const sname = self.type_aliases.get(target_ty.struct_type) orelse target_ty.struct_type; + if (self.struct_types.get(sname)) |si| { + return c.LLVMBuildLoad2(self.builder, si.llvm_type, alloca, "struct_val"); + } + return alloca; } // Array literal with target array type: generate with element conversion @@ -2893,9 +3044,20 @@ pub const CodeGen = struct { } } - const val = try self.genExpr(node); + var val = try self.genExpr(node); const src_ty = self.inferType(node); + // Struct literals return alloca pointers — load the value for by-value passing + if (src_ty.isStruct() and target_ty.isStruct()) { + if (c.LLVMGetTypeKind(c.LLVMTypeOf(val)) == c.LLVMPointerTypeKind) { + const info = self.struct_types.get(src_ty.struct_type) orelse + self.struct_types.get(self.type_aliases.get(src_ty.struct_type) orelse src_ty.struct_type); + if (info) |si| { + val = c.LLVMBuildLoad2(self.builder, si.llvm_type, val, "struct_load"); + } + } + } + // Scalar to vector broadcast if (target_ty.isVector() and !src_ty.isVector()) { const elem_ty = target_ty.vectorElementType() orelse return self.emitError("cannot determine vector element type"); @@ -2925,6 +3087,13 @@ pub const CodeGen = struct { // Same type → return as-is if (std.meta.eql(src_ty, target_ty)) return val; + // string <-> []u8: identical LLVM type {ptr, i64}, no conversion needed + if ((src_ty == .string_type and target_ty.isSlice() and + std.mem.eql(u8, target_ty.slice_type.element_name, "u8")) or + (src_ty.isSlice() and std.mem.eql(u8, src_ty.slice_type.element_name, "u8") and + target_ty == .string_type)) + return val; + const target_llvm = self.typeToLLVM(target_ty); // Any → concrete type: extract the i64 value and convert @@ -3014,7 +3183,7 @@ pub const CodeGen = struct { if (src_ty.isUnion() and target_ty.isInt()) { const uname = src_ty.union_type; if (self.union_types.get(uname)) |info| { - const tmp = c.LLVMBuildAlloca(self.builder, info.llvm_type, "union_cast"); + const tmp = self.buildEntryBlockAlloca(info.llvm_type, "union_cast"); _ = c.LLVMBuildStore(self.builder, val, tmp); const tag_ptr = c.LLVMBuildStructGEP2(self.builder, info.llvm_type, tmp, 0, "tag_ptr"); const tag_val = c.LLVMBuildLoad2(self.builder, c.LLVMInt32TypeInContext(self.context), tag_ptr, "tag_val"); @@ -3238,7 +3407,7 @@ pub const CodeGen = struct { const uinfo = self.union_types.get(val_ty.union_type) orelse return self.emitErrorFmt("unknown union type '{s}'", .{val_ty.union_type}); - const union_alloca = c.LLVMBuildAlloca(self.builder, uinfo.llvm_type, "fv_union"); + const union_alloca = self.buildEntryBlockAlloca(uinfo.llvm_type, "fv_union"); _ = c.LLVMBuildStore(self.builder, val, union_alloca); // Read tag (field 0) @@ -3313,7 +3482,7 @@ pub const CodeGen = struct { return self.emitErrorFmt("unknown array element type '{s}'", .{ainfo.element_name}); const arr_llvm_ty = self.typeToLLVM(val_ty); const elem_llvm_ty = self.typeToLLVM(elem_ty); - const arr_alloca = c.LLVMBuildAlloca(self.builder, arr_llvm_ty, "fv_arr"); + const arr_alloca = self.buildEntryBlockAlloca(arr_llvm_ty, "fv_arr"); _ = c.LLVMBuildStore(self.builder, val, arr_alloca); const idx = try self.genExpr(call_node.args[1]); var gep_indices = [_]c.LLVMValueRef{ @@ -3337,7 +3506,7 @@ pub const CodeGen = struct { const n = info.field_names.len; // Store struct to alloca BEFORE the switch (switch is a terminator) - const struct_alloca = c.LLVMBuildAlloca(self.builder, info.llvm_type, "fv_struct"); + const struct_alloca = self.buildEntryBlockAlloca(info.llvm_type, "fv_struct"); _ = c.LLVMBuildStore(self.builder, struct_val, struct_alloca); // Generate switch on idx with N cases @@ -3945,6 +4114,14 @@ pub const CodeGen = struct { const name_z = try self.allocator.dupeZ(u8, callee_name); var callee_fn = c.LLVMGetNamedFunction(self.module, name_z.ptr); + // Foreign function fallback: qualified name "ns.Func" → try unqualified "Func" (the C symbol) + if (callee_fn == null) { + if (std.mem.lastIndexOfScalar(u8, callee_name, '.')) |dot_idx| { + const base_name = callee_name[dot_idx + 1 ..]; + const base_z = try self.allocator.dupeZ(u8, base_name); + callee_fn = c.LLVMGetNamedFunction(self.module, base_z.ptr); + } + } // Intra-namespace fallback: try qualified name if (callee_fn == null) { if (self.current_namespace) |ns| { @@ -4024,7 +4201,7 @@ pub const CodeGen = struct { } else if (var_arg_count > 0) { // Allocate array on stack: [N x elem_type] const arr_ty = c.LLVMArrayType2(elem_llvm_ty, @intCast(var_arg_count)); - const arr_alloca = c.LLVMBuildAlloca(self.builder, arr_ty, "varargs_arr"); + const arr_alloca = self.buildEntryBlockAlloca(arr_ty, "varargs_arr"); // Store each variadic arg for (0..var_arg_count) |vi_idx| { const arg_val = if (elem_ty.isAny()) blk: { @@ -4068,7 +4245,34 @@ pub const CodeGen = struct { stored_param_types.?[i] else self.llvmTypeToSxType(param_llvm_types[i]); - try arg_vals.append(self.allocator, try self.genExprAsType(arg, param_ty)); + // For #foreign functions, [:0]u8 params have LLVM type ptr + const arg_ty = self.inferType(arg); + const llvm_param_is_ptr = (i < num_params and + c.LLVMGetTypeKind(param_llvm_types[i]) == c.LLVMPointerTypeKind); + const ptr_ty = Type{ .pointer_type = .{ .pointee_name = "u8" } }; + + var val = if (llvm_param_is_ptr and arg.data == .string_literal) blk: { + // String literal → pointer: produce raw ptr directly (context-dependent) + break :blk try self.genExprAsType(arg, ptr_ty); + } else if (llvm_param_is_ptr and arg_ty == .string_type) blk: { + // String variable → pointer: extract .ptr from {ptr, len} + const str_val = try self.genExpr(arg); + break :blk c.LLVMBuildExtractValue(self.builder, str_val, 0, "str_ptr"); + } else if ((param_ty.isPointer() or llvm_param_is_ptr) and arg_ty.isSlice() and + std.mem.eql(u8, arg_ty.slice_type.element_name, "u8")) + { + return self.emitError( + "cannot pass []u8 to *u8: slice may not be null-terminated; use a string literal or xx cast", + ); + } else try self.genExprAsType(arg, param_ty); + // Foreign calls: convert struct values to C ABI representation + if (param_ty.isStruct() and self.foreign_fns.contains(callee_name)) { + const struct_llvm_ty = self.typeToLLVM(param_ty); + if (struct_llvm_ty != param_llvm_types[i]) { + val = self.convertStructToABI(val, struct_llvm_ty, param_llvm_types[i]); + } + } + try arg_vals.append(self.allocator, val); } else { try arg_vals.append(self.allocator, try self.genExpr(arg)); } @@ -4886,9 +5090,9 @@ pub const CodeGen = struct { const elem_llvm_ty = self.typeToLLVM(elem_ty); // Allocate it_index (s64) and it (element type) - const idx_alloca = c.LLVMBuildAlloca(self.builder, i64_type, "it_index"); + const idx_alloca = self.buildEntryBlockAlloca(i64_type, "it_index"); _ = c.LLVMBuildStore(self.builder, c.LLVMConstInt(i64_type, 0, 0), idx_alloca); - const it_alloca = c.LLVMBuildAlloca(self.builder, elem_llvm_ty, "it"); + const it_alloca = self.buildEntryBlockAlloca(elem_llvm_ty, "it"); // Push scope and bind it, it_index try self.pushScope(); @@ -5602,7 +5806,7 @@ pub const CodeGen = struct { } if (obj_ty == .string_type) { if (std.mem.eql(u8, fa.field, "len")) return Type.s(64); - if (std.mem.eql(u8, fa.field, "ptr")) return .string_type; + if (std.mem.eql(u8, fa.field, "ptr")) return .{ .pointer_type = .{ .pointee_name = "u8" } }; } if (obj_ty.isSlice()) { if (std.mem.eql(u8, fa.field, "len")) return Type.s(64); @@ -5654,7 +5858,7 @@ pub const CodeGen = struct { }, .slice_expr => |se| { const obj_ty = self.inferType(se.object); - if (obj_ty == .string_type) return .string_type; + if (obj_ty == .string_type) return .{ .slice_type = .{ .element_name = "u8" } }; if (obj_ty.isArray()) return .{ .slice_type = .{ .element_name = obj_ty.array_type.element_name } }; if (obj_ty.isSlice()) return obj_ty; return .void_type; @@ -5726,9 +5930,23 @@ pub const CodeGen = struct { } } - pub fn link(io: std.Io, output_obj: []const u8, output_bin: []const u8) !void { + pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, output_bin: []const u8, libraries: []const []const u8) !void { + var argv = std.ArrayList([]const u8).empty; + try argv.appendSlice(allocator, &.{ "cc", output_obj, "-o", output_bin }); + + if (libraries.len > 0) { + // Add Homebrew library path on macOS + try argv.append(allocator, "-L/opt/homebrew/lib"); + + for (libraries) |lib| { + const flag = try std.fmt.allocPrint(allocator, "-l{s}", .{lib}); + try argv.append(allocator, flag); + } + } + + const argv_slice = try argv.toOwnedSlice(allocator); var child = std.process.spawn(io, .{ - .argv = &.{ "cc", output_obj, "-o", output_bin }, + .argv = argv_slice, }) catch return error.LinkError; const result = child.wait(io) catch return error.LinkError; if (result != .exited) return error.LinkError; diff --git a/src/lexer.zig b/src/lexer.zig index b3d6d41..5b0e094 100644 --- a/src/lexer.zig +++ b/src/lexer.zig @@ -50,13 +50,15 @@ pub const Lexer = struct { return self.lexString(start); } - // Directives: #import, #insert, #run, #builtin + // Directives: #import, #insert, #run, #builtin, #foreign, #library if (c == '#') { const directives = .{ .{ "#import", Tag.hash_import }, .{ "#insert", Tag.hash_insert }, .{ "#run", Tag.hash_run }, .{ "#builtin", Tag.hash_builtin }, + .{ "#foreign", Tag.hash_foreign }, + .{ "#library", Tag.hash_library }, }; inline for (directives) |d| { const keyword = d[0]; @@ -370,6 +372,25 @@ test "lex hash_insert" { try std.testing.expectEqual(Tag.invalid, lex2.next().tag); } +test "lex hash_foreign" { + var lex = Lexer.init("#foreign"); + try std.testing.expectEqual(Tag.hash_foreign, lex.next().tag); + try std.testing.expectEqual(Tag.eof, lex.next().tag); + + var lex2 = Lexer.init("#foreignx"); + try std.testing.expectEqual(Tag.invalid, lex2.next().tag); +} + +test "lex hash_library" { + var lex = Lexer.init("#library \"raylib\""); + try std.testing.expectEqual(Tag.hash_library, lex.next().tag); + try std.testing.expectEqual(Tag.string_literal, lex.next().tag); + try std.testing.expectEqual(Tag.eof, lex.next().tag); + + var lex2 = Lexer.init("#librarypath"); + try std.testing.expectEqual(Tag.invalid, lex2.next().tag); +} + test "lex string" { var lex = Lexer.init("\"Hello\""); const tok = lex.next(); diff --git a/src/lsp/server.zig b/src/lsp/server.zig index f97c314..12029ad 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -717,6 +717,8 @@ pub const Server = struct { .hash_import, .hash_insert, .hash_builtin, + .hash_foreign, + .hash_library, => ST.keyword, // Type keywords diff --git a/src/main.zig b/src/main.zig index 0093c95..dbe6362 100644 --- a/src/main.zig +++ b/src/main.zig @@ -148,7 +148,7 @@ fn compile(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, out cg.emitObject(obj_path.ptr) catch { comp.renderErrors(); return error.CompileError; }; // Link - sx.codegen.CodeGen.link(io, obj_path, output_path) catch { + sx.codegen.CodeGen.link(allocator, io, obj_path, output_path, cg.foreign_libraries.items) catch { std.debug.print("error: linking failed\n", .{}); return error.CompileError; }; diff --git a/src/parser.zig b/src/parser.zig index d7b1d80..ebed879 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -62,6 +62,19 @@ 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(); @@ -179,6 +192,15 @@ 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 + if (self.current.tag == .hash_foreign) { + const fi_start = self.current.loc.start; + self.advance(); + try self.expect(.semicolon); + const fi = try self.createNode(fi_start, .{ .foreign_expr = {} }); + return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = value, .value = fi } }); + } + try self.expect(.semicolon); return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = null, .value = value } }); } @@ -223,9 +245,24 @@ pub const Parser = struct { return try self.createNode(start, .{ .pointer_type_expr = .{ .pointee_type = pointee_type } }); } - // Array type: [N]T, Slice type: []T, Many-pointer type: [*]T + // Array type: [N]T, Slice type: []T, Many-pointer type: [*]T, Sentinel slice: [:0]T if (self.current.tag == .l_bracket) { self.advance(); // skip '[' + if (self.current.tag == .colon) { + // Sentinel-terminated slice: [:0]T + self.advance(); // skip ':' + if (self.current.tag != .int_literal) { + return self.fail("expected sentinel value after ':'"); + } + const sentinel_str = self.tokenSlice(self.current); + self.advance(); // skip sentinel value + try self.expect(.r_bracket); // expect ']' + const elem_type = try self.parseTypeExpr(); + // Build name like "[:0]u8" for type resolution + const elem_name = if (elem_type.data == .type_expr) elem_type.data.type_expr.name else "?"; + const name = try std.fmt.allocPrint(self.allocator, "[:{s}]{s}", .{ sentinel_str, elem_name }); + return try self.createNode(start, .{ .type_expr = .{ .name = name } }); + } if (self.current.tag == .r_bracket) { // Slice type: []T self.advance(); // skip ']' @@ -621,12 +658,17 @@ pub const Parser = struct { return_type = try self.parseTypeExpr(); } - // Body: block `{ ... }`, arrow `=> expr;`, or #builtin marker + // Body: block `{ ... }`, arrow `=> expr;`, #builtin, or #foreign marker const body = if (self.current.tag == .hash_builtin) blk: { const bi_start = self.current.loc.start; self.advance(); try self.expect(.semicolon); break :blk try self.createNode(bi_start, .{ .builtin_expr = {} }); + } else if (self.current.tag == .hash_foreign) blk: { + const fi_start = self.current.loc.start; + self.advance(); + try self.expect(.semicolon); + break :blk try self.createNode(fi_start, .{ .foreign_expr = {} }); } else if (self.current.tag == .fat_arrow) blk: { self.advance(); const expr = try self.parseExpr(); @@ -1409,8 +1451,8 @@ pub const Parser = struct { } if (self.current.tag == .r_paren) { self.advance(); // skip ')' - // Function if followed by '{', '->', '#builtin', or '=>' - return self.current.tag == .l_brace or self.current.tag == .arrow or self.current.tag == .hash_builtin or self.current.tag == .fat_arrow; + // Function if followed by '{', '->', '#builtin', '#foreign', or '=>' + return self.current.tag == .l_brace or self.current.tag == .arrow or self.current.tag == .hash_builtin or self.current.tag == .hash_foreign or self.current.tag == .fat_arrow; } return false; } @@ -1582,6 +1624,18 @@ test "parse namespaced import" { try std.testing.expectEqualStrings("std", decl.data.import_decl.name.?); } +test "parse library declaration" { + const source = "#library \"raylib\";"; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), source); + const root = try parser.parse(); + try std.testing.expectEqual(@as(usize, 1), root.data.root.decls.len); + 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); +} + test "parse void function with builtin body" { const source = "foo :: () #builtin;"; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); @@ -1595,6 +1649,20 @@ test "parse void function with builtin body" { try std.testing.expect(decl.data.fn_decl.body.data == .builtin_expr); } +test "parse void function with foreign body" { + const source = "InitWindow :: (width: s32, height: s32, title: *u8) -> void #foreign;"; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), source); + const root = try parser.parse(); + try std.testing.expectEqual(@as(usize, 1), root.data.root.decls.len); + const decl = root.data.root.decls[0]; + 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.expectEqual(@as(usize, 3), decl.data.fn_decl.params.len); +} + test "parse void function with arrow body" { const source = "foo :: () => 42;"; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); diff --git a/src/sema.zig b/src/sema.zig index eaa5cd4..f4da109 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -671,6 +671,8 @@ pub const Analyzer = struct { .match_arm, .undef_literal, .builtin_expr, + .foreign_expr, + .library_decl, .import_decl, .array_type_expr, .slice_type_expr, @@ -925,6 +927,8 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node { .match_arm, .undef_literal, .builtin_expr, + .foreign_expr, + .library_decl, .enum_decl, .struct_decl, .union_decl, diff --git a/src/token.zig b/src/token.zig index 574448e..6037867 100644 --- a/src/token.zig +++ b/src/token.zig @@ -77,6 +77,8 @@ pub const Tag = enum { hash_import, // #import hash_insert, // #insert hash_builtin, // #builtin + hash_foreign, // #foreign + hash_library, // #library triple_minus, // --- // Special diff --git a/src/types.zig b/src/types.zig index 551e429..2700546 100644 --- a/src/types.zig +++ b/src/types.zig @@ -66,6 +66,17 @@ pub const Type = union(enum) { if (std.mem.eql(u8, name, "f32")) return .f32; if (std.mem.eql(u8, name, "f64")) return .f64; if (std.mem.eql(u8, name, "Any")) return .any_type; + // Sentinel-terminated slice: [:0]T → string_type when T is u8 + if (name.len >= 5 and name[0] == '[' and name[1] == ':') { + // Find closing ']' + if (std.mem.indexOfScalar(u8, name, ']')) |close| { + const sentinel = name[2..close]; + const elem = name[close + 1 ..]; + if (std.mem.eql(u8, sentinel, "0") and std.mem.eql(u8, elem, "u8")) { + return .string_type; + } + } + } // Many-pointer: [*]T if (name.len >= 4 and name[0] == '[' and name[1] == '*' and name[2] == ']') { return .{ .many_pointer_type = .{ .element_name = name[3..] } }; @@ -116,6 +127,19 @@ pub const Type = union(enum) { }; } + pub fn isString(self: Type) bool { + return self == .string_type; + } + + /// Returns true for both `string` (null-terminated) and `[]u8` (byte slice) + pub fn isStringLike(self: Type) bool { + if (self == .string_type) return true; + if (self.isSlice()) { + return std.mem.eql(u8, self.slice_type.element_name, "u8"); + } + return false; + } + pub fn isSlice(self: Type) bool { return switch (self) { .slice_type => true, @@ -226,10 +250,19 @@ pub const Type = union(enum) { /// Everything else requires `xx`. pub fn isImplicitlyConvertibleTo(self: Type, target: Type) bool { if (std.meta.eql(self, target)) return true; + // Struct types: compare names by content (not pointer identity) + if (self.isStruct() and target.isStruct()) { + return std.mem.eql(u8, self.struct_type, target.struct_type); + } // Slice types: compare element names by content (not pointer) if (self.isSlice() and target.isSlice()) { return std.mem.eql(u8, self.slice_type.element_name, target.slice_type.element_name); } + // string <-> []u8: same layout, bidirectional implicit conversion + if (self == .string_type and target.isSlice() and + std.mem.eql(u8, target.slice_type.element_name, "u8")) return true; + if (self.isSlice() and std.mem.eql(u8, self.slice_type.element_name, "u8") and + target == .string_type) return true; // Pointer types: compare pointee names by content, *void is universal (both directions) if (self.isPointer() and target.isPointer()) { if (std.mem.eql(u8, self.pointer_type.pointee_name, "void")) return true;