diff --git a/examples/10-generic-struct.sx b/examples/10-generic-struct.sx index 6123f49..0e21584 100644 --- a/examples/10-generic-struct.sx +++ b/examples/10-generic-struct.sx @@ -1,4 +1,5 @@ #import "modules/std.sx"; +#import "modules/math"; Vec :: struct($N: u32, $T:Type) { // (LLVM Vector) diff --git a/examples/modules/compiler.sx b/examples/modules/compiler.sx index 6a3cf38..bf1ab32 100644 --- a/examples/modules/compiler.sx +++ b/examples/modules/compiler.sx @@ -6,14 +6,8 @@ ARCH : Architecture = .unknown; POINTER_SIZE : s64 = 8; BuildOptions :: struct { - add_link_flag :: (self: BuildOptions, flag: [:0]u8) { - // Compiler builtin — intercepted at compile time - } - set_output_path :: (self: BuildOptions, path: [:0]u8) { - // Compiler builtin — intercepted at compile time - } + add_link_flag :: (self: BuildOptions, flag: [:0]u8) #compiler; + set_output_path :: (self: BuildOptions, path: [:0]u8) #compiler; } -build_options :: () -> BuildOptions { - return BuildOptions.{}; -} +build_options :: () -> BuildOptions #compiler; diff --git a/src/ast.zig b/src/ast.zig index 0a52db7..6a34b65 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -67,6 +67,7 @@ pub const Node = struct { undef_literal: void, inferred_type: void, builtin_expr: void, + compiler_expr: void, foreign_expr: ForeignExpr, library_decl: LibraryDecl, function_type_expr: FunctionTypeExpr, diff --git a/src/ir/compiler_hooks.zig b/src/ir/compiler_hooks.zig new file mode 100644 index 0000000..a27393d --- /dev/null +++ b/src/ir/compiler_hooks.zig @@ -0,0 +1,101 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const interp_mod = @import("interp.zig"); +const Value = interp_mod.Value; +const Interpreter = interp_mod.Interpreter; + +// ── BuildConfig ───────────────────────────────────────────────────────── +// Mutable build configuration accumulated by #run blocks via #compiler methods. + +pub const BuildConfig = struct { + link_flags: std.ArrayList([]const u8) = .empty, + output_path: ?[]const u8 = null, + + pub fn deinit(self: *BuildConfig, alloc: Allocator) void { + self.link_flags.deinit(alloc); + } +}; + +// ── Hook system ───────────────────────────────────────────────────────── + +pub const HookError = error{ + CannotEvalComptime, + TypeError, +}; + +/// Hook function signature. Receives the interpreter (for heap/string access), +/// resolved argument values, and the mutable build config. +pub const HookFn = *const fn ( + interp: *const Interpreter, + args: []const Value, + bc: *BuildConfig, + alloc: Allocator, +) HookError!Value; + +pub const Registry = struct { + hooks: std.StringHashMap(HookFn), + + pub fn init(alloc: Allocator) Registry { + return .{ .hooks = std.StringHashMap(HookFn).init(alloc) }; + } + + pub fn deinit(self: *Registry) void { + self.hooks.deinit(); + } + + pub fn get(self: *const Registry, name: []const u8) ?HookFn { + return self.hooks.get(name); + } + + /// Register all built-in compiler hooks. + pub fn registerDefaults(self: *Registry) void { + self.hooks.put("build_options", &hookBuildOptions) catch {}; + self.hooks.put("BuildOptions.add_link_flag", &hookAddLinkFlag) catch {}; + self.hooks.put("BuildOptions.set_output_path", &hookSetOutputPath) catch {}; + } +}; + +// ── build_options() hook ──────────────────────────────────────────────── + +fn hookBuildOptions( + _: *const Interpreter, + _: []const Value, + _: *BuildConfig, + _: Allocator, +) HookError!Value { + // build_options() returns a sentinel value; the real work happens + // when methods like add_link_flag/set_output_path are called on it. + return .void_val; +} + +// ── BuildOptions hooks ────────────────────────────────────────────────── + +fn hookAddLinkFlag( + interp: *const Interpreter, + args: []const Value, + bc: *BuildConfig, + alloc: Allocator, +) HookError!Value { + // args: [self (BuildOptions value), flag_string] + if (args.len < 2) return .void_val; + const str_val = args[1]; + if (str_val.asString(interp)) |s| { + bc.link_flags.append(alloc, alloc.dupe(u8, s) catch return error.CannotEvalComptime) catch return error.CannotEvalComptime; + } + return .void_val; +} + +fn hookSetOutputPath( + interp: *const Interpreter, + args: []const Value, + bc: *BuildConfig, + alloc: Allocator, +) HookError!Value { + // args: [self (BuildOptions value), path_string] + if (args.len < 2) return .void_val; + const str_val = args[1]; + if (str_val.asString(interp)) |s| { + bc.output_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime; + } + return .void_val; +} diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index a756229..9eee79c 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -1454,6 +1454,10 @@ pub const LLVMEmitter = struct { }, } }, + .compiler_call => { + // Compiler hooks are comptime-only; if one reaches emission, produce undef + self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); + }, .call_closure => |call_op| { // Closure: { fn_ptr, env } — extract fn_ptr, prepend env as first arg const closure = self.resolveRef(call_op.callee); diff --git a/src/ir/inst.zig b/src/ir/inst.zig index 17bec08..b2985f3 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -177,6 +177,7 @@ pub const Op = union(enum) { call_indirect: CallIndirect, call_closure: CallIndirect, call_builtin: BuiltinCall, + compiler_call: CompilerCall, // ── Protocol dispatch ─────────────────────────────────────────── protocol_call_dynamic: ProtocolCall, // vtable/inline dispatch @@ -302,9 +303,11 @@ pub const BuiltinId = enum(u16) { type_of, alloc, dealloc, - build_options, - build_options_add_link_flag, - build_options_set_output_path, +}; + +pub const CompilerCall = struct { + name: u32, // StringPool id for qualified name (e.g. "BuildOptions.add_link_flag") + args: []const Ref, }; pub const ProtocolCall = struct { diff --git a/src/ir/interp.zig b/src/ir/interp.zig index 1708cb1..a406ef8 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -106,17 +106,8 @@ pub const InterpError = error{ Unreachable, }; -// ── BuildConfig ───────────────────────────────────────────────────────── -// Mutable build configuration accumulated by #run blocks via BuildOptions methods. - -pub const BuildConfig = struct { - link_flags: std.ArrayList([]const u8) = .empty, - output_path: ?[]const u8 = null, - - pub fn deinit(self: *BuildConfig, alloc: Allocator) void { - self.link_flags.deinit(alloc); - } -}; +const compiler_hooks = @import("compiler_hooks.zig"); +pub const BuildConfig = compiler_hooks.BuildConfig; // ── Interpreter ───────────────────────────────────────────────────────── @@ -136,13 +127,19 @@ pub const Interpreter = struct { // Mutable build configuration — set by LLVMEmitter, written by #run blocks build_config: ?*BuildConfig = null, + // Compiler hook registry for #compiler methods + hooks: compiler_hooks.Registry, + pub fn init(module: *const Module, alloc: Allocator) Interpreter { + var hooks = compiler_hooks.Registry.init(alloc); + hooks.registerDefaults(); return .{ .module = module, .alloc = alloc, .output = std.ArrayList(u8).empty, .heap = std.ArrayList([]u8).empty, .global_values = std.AutoHashMap(u32, Value).init(alloc), + .hooks = hooks, }; } @@ -154,6 +151,7 @@ pub const Interpreter = struct { self.heap.deinit(self.alloc); self.output.deinit(self.alloc); self.global_values.deinit(); + self.hooks.deinit(); } // ── Heap operations ──────────────────────────────────────────── @@ -618,6 +616,25 @@ pub const Interpreter = struct { return self.execBuiltin(bi, frame, instruction.ty); }, + // ── Compiler hook calls (#compiler methods) ──────── + .compiler_call => |cc| { + const name = self.module.types.getString(@enumFromInt(cc.name)); + if (self.hooks.get(name)) |hook| { + // Resolve args from Ref to Value + var resolved_args = std.ArrayList(Value).empty; + defer resolved_args.deinit(self.alloc); + for (cc.args) |arg| { + resolved_args.append(self.alloc, frame.getRef(arg)) catch return error.CannotEvalComptime; + } + if (self.build_config) |bc| { + const result = hook(self, resolved_args.items, bc, self.alloc) catch return error.CannotEvalComptime; + return .{ .value = result }; + } + return .{ .value = .void_val }; + } + return error.CannotEvalComptime; + }, + // ── Struct GEP (field pointer) ───────────────────── .struct_gep => |fa| { const base = frame.getRef(fa.base); @@ -1257,30 +1274,6 @@ pub const Interpreter = struct { const f = val.asFloat() orelse return error.TypeError; return .{ .value = .{ .float = @floor(f) } }; }, - .build_options => { - // Returns a void sentinel — the "handle" to BuildConfig - return .{ .value = .void_val }; - }, - .build_options_add_link_flag => { - // args: [opts_handle, flag_string] - const str_val = frame.getRef(bi.args[1]); - if (str_val.asString(self)) |s| { - if (self.build_config) |bc| { - bc.link_flags.append(self.alloc, self.alloc.dupe(u8, s) catch return error.CannotEvalComptime) catch return error.CannotEvalComptime; - } - } - return .{ .value = .void_val }; - }, - .build_options_set_output_path => { - // args: [opts_handle, path_string] - const str_val = frame.getRef(bi.args[1]); - if (str_val.asString(self)) |s| { - if (self.build_config) |bc| { - bc.output_path = self.alloc.dupe(u8, s) catch return error.CannotEvalComptime; - } - } - return .{ .value = .void_val }; - }, .cast, .type_of, .alloc, .dealloc => { return error.CannotEvalComptime; }, diff --git a/src/ir/ir.zig b/src/ir/ir.zig index 58ad440..bdb870c 100644 --- a/src/ir/ir.zig +++ b/src/ir/ir.zig @@ -31,6 +31,7 @@ pub const Interpreter = interp.Interpreter; pub const Value = interp.Value; pub const Lowering = lower.Lowering; +pub const compiler_hooks = @import("compiler_hooks.zig"); pub const emit_llvm = @import("emit_llvm.zig"); pub const LLVMEmitter = emit_llvm.LLVMEmitter; diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 537d22e..2f2f1b9 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -534,7 +534,7 @@ pub const Lowering = struct { // No AST? (builtins, foreign functions, or imported functions not in this file) const fd = self.fn_ast_map.get(name) orelse return; // Check builtin/foreign/generic — these stay as extern stubs - if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr) return; + if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr or fd.body.data == .compiler_expr) return; if (fd.type_params.len > 0) return; // generics handled by monomorphization (Step 3.13) // Defer functions with type-category matches until all types are registered. @@ -685,7 +685,7 @@ pub const Lowering = struct { } // Check if the function body is a builtin or foreign declaration (no body needed) - if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr) { + if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr or fd.body.data == .compiler_expr) { // Already declared by scanDecls/declareFunction (which handles #foreign renames) return; } @@ -3707,6 +3707,14 @@ pub const Lowering = struct { return self.lowerGenericCall(fd, func_name, c, args.items); } } + // Check for #compiler free functions + if (self.fn_ast_map.get(func_name)) |fd_check| { + if (fd_check.body.data == .compiler_expr) { + const ret_ty = if (fd_check.return_type) |rt| type_bridge.resolveAstType(rt, &self.module.types) else TypeId.void; + return self.builder.compilerCall(func_name, args.items, ret_ty); + } + } + // Look up declared/extern function — try lazy lowering if not yet lowered { // First attempt: function may already be declared (from scanDecls) @@ -3960,18 +3968,16 @@ pub const Lowering = struct { // Try to resolve the method by struct type name const struct_name = self.getStructTypeName(obj_ty); if (struct_name) |sname| { - // Intercept BuildOptions compiler builtins - if (std.mem.eql(u8, sname, "BuildOptions")) { - if (std.mem.eql(u8, fa.field, "add_link_flag")) { - return self.builder.callBuiltin(.build_options_add_link_flag, method_args.items, .void); - } else if (std.mem.eql(u8, fa.field, "set_output_path")) { - return self.builder.callBuiltin(.build_options_set_output_path, method_args.items, .void); - } - } - // Try direct qualified name: StructName.method const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch fa.field; + // Generic #compiler method dispatch + if (self.fn_ast_map.get(qualified)) |method_fd| { + if (method_fd.body.data == .compiler_expr) { + return self.builder.compilerCall(qualified, method_args.items, .void); + } + } + // Check for generic struct template method if (self.struct_instance_template.get(sname)) |tmpl_name| { // This is an instantiated generic struct — look up template method @@ -7595,13 +7601,14 @@ pub const Lowering = struct { const oi = self.module.types.get(obj_ty); if (oi == .@"struct") { const struct_name = self.module.types.getString(oi.@"struct".name); - // Intercept BuildOptions compiler builtins - if (std.mem.eql(u8, struct_name, "BuildOptions")) { - if (std.mem.eql(u8, cfa.field, "add_link_flag") or std.mem.eql(u8, cfa.field, "set_output_path")) { + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ struct_name, cfa.field }) catch cfa.field; + // Generic #compiler method dispatch — return type from declaration + if (self.fn_ast_map.get(qualified)) |method_fd| { + if (method_fd.body.data == .compiler_expr) { + if (method_fd.return_type) |rt| return type_bridge.resolveAstType(rt, &self.module.types); return .void; } } - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ struct_name, cfa.field }) catch cfa.field; if (self.resolveFuncByName(qualified)) |fid| { return self.module.functions.items[@intFromEnum(fid)].ret; } diff --git a/src/ir/module.zig b/src/ir/module.zig index 1a8c905..fe92b24 100644 --- a/src/ir/module.zig +++ b/src/ir/module.zig @@ -362,6 +362,12 @@ pub const Builder = struct { return self.emit(.{ .call_builtin = .{ .builtin = builtin, .args = owned } }, ret_ty); } + pub fn compilerCall(self: *Builder, name: []const u8, args: []const Ref, ret_ty: TypeId) Ref { + const name_id = self.module.types.strings.intern(self.module.alloc, name); + const owned = self.module.alloc.dupe(Ref, args) catch unreachable; + return self.emit(.{ .compiler_call = .{ .name = @intFromEnum(name_id), .args = owned } }, ret_ty); + } + // ── Protocol ──────────────────────────────────────────────────── pub fn protocolCallDynamic(self: *Builder, receiver: Ref, method_index: u32, args: []const Ref, ret_ty: TypeId) Ref { diff --git a/src/ir/print.zig b/src/ir/print.zig index 7697026..74e37de 100644 --- a/src/ir/print.zig +++ b/src/ir/print.zig @@ -316,6 +316,12 @@ fn printInst(instruction: *const Inst, ref_idx: u32, tt: *const TypeTable, write try writeArgs(c.args, writer); try writer.writeAll(") : "); }, + .compiler_call => |cc| { + const name = tt.getString(@enumFromInt(cc.name)); + try writer.print("compiler_call \"{s}\"(", .{name}); + try writeArgs(cc.args, writer); + try writer.writeAll(") : "); + }, // ── Protocol ──────────────────────────────────────────── .protocol_call_dynamic => |c| { diff --git a/src/lexer.zig b/src/lexer.zig index 4fcde1f..c2133c8 100644 --- a/src/lexer.zig +++ b/src/lexer.zig @@ -69,6 +69,7 @@ pub const Lexer = struct { .{ "#insert", Tag.hash_insert }, .{ "#run", Tag.hash_run }, .{ "#builtin", Tag.hash_builtin }, + .{ "#compiler", Tag.hash_compiler }, .{ "#foreign", Tag.hash_foreign }, .{ "#library", Tag.hash_library }, .{ "#using", Tag.hash_using }, diff --git a/src/lsp/server.zig b/src/lsp/server.zig index ee10b77..318225c 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -1482,6 +1482,7 @@ pub const Server = struct { .hash_import, .hash_insert, .hash_builtin, + .hash_compiler, .hash_foreign, .hash_library, .hash_using, diff --git a/src/parser.zig b/src/parser.zig index b2a7c8e..e443f2d 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -1215,13 +1215,18 @@ pub const Parser = struct { return_type = try self.parseTypeExpr(); } - // Body: block `{ ... }`, arrow `=> expr;`, #builtin, or #foreign marker + // Body: block `{ ... }`, arrow `=> expr;`, #builtin, #compiler, or #foreign marker var is_arrow = false; 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_compiler) blk: { + const ci_start = self.current.loc.start; + self.advance(); + try self.expect(.semicolon); + break :blk try self.createNode(ci_start, .{ .compiler_expr = {} }); } else if (self.current.tag == .hash_foreign) blk: { const fi_start = self.current.loc.start; self.advance(); @@ -2351,7 +2356,7 @@ pub const Parser = struct { fn isFunctionDef(self: *Parser) bool { const tag = self.peekPastParens() orelse return false; - return tag == .l_brace or tag == .arrow or tag == .hash_builtin or tag == .hash_foreign or tag == .fat_arrow; + return tag == .l_brace or tag == .arrow or tag == .hash_builtin or tag == .hash_compiler or tag == .hash_foreign or tag == .fat_arrow; } fn isAssignOp(self: *const Parser) bool { diff --git a/src/sema.zig b/src/sema.zig index be42b54..0c36e9e 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -862,6 +862,7 @@ pub const Analyzer = struct { .undef_literal, .inferred_type, .builtin_expr, + .compiler_expr, .foreign_expr, .library_decl, .function_type_expr, @@ -1264,6 +1265,7 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node { .undef_literal, .inferred_type, .builtin_expr, + .compiler_expr, .foreign_expr, .library_decl, .function_type_expr, diff --git a/src/token.zig b/src/token.zig index 87df06c..b3d832e 100644 --- a/src/token.zig +++ b/src/token.zig @@ -101,6 +101,7 @@ pub const Tag = enum { hash_import, // #import hash_insert, // #insert hash_builtin, // #builtin + hash_compiler, // #compiler hash_foreign, // #foreign hash_library, // #library hash_using, // #using