From 0336f361c779d02966f6f3c34406a73ffb3acad7 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 3 Mar 2026 16:18:58 +0200 Subject: [PATCH] issue 06 --- examples/50-smoke.sx | 1 + examples/modules/sdl3.sx | 2 + examples/modules/test.sx | 5 + src/core.zig | 6 +- src/ir/lower.zig | 194 +++++++++++++++++++++++++++++++++------ src/main.zig | 2 +- src/parser.zig | 4 +- 7 files changed, 181 insertions(+), 33 deletions(-) create mode 100644 examples/modules/test.sx diff --git a/examples/50-smoke.sx b/examples/50-smoke.sx index 9759e45..ffb022b 100644 --- a/examples/50-smoke.sx +++ b/examples/50-smoke.sx @@ -1,6 +1,7 @@ #import "modules/std.sx"; #import "modules/math/math.sx"; #import "modules/compiler.sx"; +#import "modules/test.sx"; pkg :: #import "modules/testpkg"; // ============================================================ diff --git a/examples/modules/sdl3.sx b/examples/modules/sdl3.sx index 923c099..249b085 100644 --- a/examples/modules/sdl3.sx +++ b/examples/modules/sdl3.sx @@ -333,6 +333,8 @@ 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_GetPerformanceCounter :: () -> u64 #foreign sdl3; +SDL_GetPerformanceFrequency :: () -> u64 #foreign sdl3; SDL_Delay :: (ms: u32) -> void #foreign sdl3; SDL_GetWindowDisplayScale :: (window: *void) -> f32 #foreign sdl3; SDL_GetWindowSize :: (window: *void, w: *s32, h: *s32) -> bool #foreign sdl3; diff --git a/examples/modules/test.sx b/examples/modules/test.sx new file mode 100644 index 0000000..5ee294f --- /dev/null +++ b/examples/modules/test.sx @@ -0,0 +1,5 @@ +assert :: (condition: bool) { + if !condition { + out("assertion failed\n"); + } +} diff --git a/src/core.zig b/src/core.zig index b771706..bb706f3 100644 --- a/src/core.zig +++ b/src/core.zig @@ -97,7 +97,7 @@ pub const Compilation = struct { pub fn generateCode(self: *Compilation) !void { // Heap-allocate the IR module so its address is stable during emit const ir_mod_ptr = try self.allocator.create(ir.Module); - ir_mod_ptr.* = self.lowerToIR(); + ir_mod_ptr.* = try self.lowerToIR(); var emitter = ir.LLVMEmitter.init(self.allocator, ir_mod_ptr, "sx_module", self.target_config); emitter.emit(); // IR module is no longer needed after LLVM IR has been generated @@ -131,7 +131,7 @@ pub const Compilation = struct { } /// Lower the parsed AST to the sx IR module (shadow pipeline). - pub fn lowerToIR(self: *Compilation) ir.Module { + pub fn lowerToIR(self: *Compilation) !ir.Module { const root = self.resolved_root orelse self.root orelse return ir.Module.init(self.allocator); var module = ir.Module.init(self.allocator); //TODO: find a better place for this @@ -142,7 +142,9 @@ pub const Compilation = struct { lowering.main_file = self.file_path; lowering.resolved_root = root; lowering.target_config = self.target_config; + lowering.diagnostics = &self.diagnostics; lowering.lowerRoot(root); + if (self.diagnostics.hasErrors()) return error.CompileError; return module; } diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 5813310..7e321f6 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -9,6 +9,7 @@ const type_bridge = @import("type_bridge.zig"); const unescape = @import("../unescape.zig"); const parser_mod = @import("../parser.zig"); const interp_mod = @import("interp.zig"); +const errors = @import("../errors.zig"); const TypeId = types.TypeId; const StringId = types.StringId; @@ -106,6 +107,7 @@ pub const Lowering = struct { ufcs_alias_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // UFCS alias name → target function name target_config: ?@import("../target.zig").TargetConfig = null, // compilation target (for inline if) comptime_constants: std.StringHashMap(ComptimeValue) = std.StringHashMap(ComptimeValue).init(std.heap.page_allocator), // compile-time known constants (e.g. OS, ARCH) + diagnostics: ?*errors.DiagnosticList = null, // error reporting with source locations pub const ComptimeValue = union(enum) { int_val: i64, @@ -1287,7 +1289,7 @@ pub const Lowering = struct { } }, else => { - _ = self.emitPlaceholder("assignment_target"); + _ = self.emitError("assignment_target", asgn.target.span); }, } } @@ -1386,7 +1388,7 @@ pub const Lowering = struct { .xor_assign => self.builder.emit(.{ .bit_xor = .{ .lhs = lhs, .rhs = rhs } }, ty), .shl_assign => self.builder.emit(.{ .shl = .{ .lhs = lhs, .rhs = rhs } }, ty), .shr_assign => self.builder.emit(.{ .shr = .{ .lhs = lhs, .rhs = rhs } }, ty), - else => self.emitPlaceholder("compound_assign"), + else => self.emitError("compound_assign", null), }; } @@ -1474,8 +1476,13 @@ pub const Lowering = struct { const str = self.builder.constString(sid); break :blk self.builder.boxAny(str, .string); } - // Unknown identifier — emit placeholder - break :blk self.emitPlaceholder(id.name); + // Type-as-value: known type name used where a value is expected (e.g. cast(s64, val)) + // The type arg is lowered but unused — the caller resolves from AST. + if (self.isKnownTypeName(id.name)) { + break :blk self.emitPlaceholder(id.name); + } + // Unknown identifier + break :blk self.emitError(id.name, node.span); }, .binary_op => |bop| self.lowerBinaryOp(&bop), @@ -1554,7 +1561,7 @@ pub const Lowering = struct { break :blk self.lowerInsertExprValue(ins.expr); }, .tuple_literal => |tl| self.lowerTupleLiteral(&tl), - .spread_expr => self.emitPlaceholder("spread_expr"), + .spread_expr => self.emitError("spread_expr", node.span), .chained_comparison => |cc| self.lowerChainedComparison(&cc), // Statements that can appear in expression position @@ -1603,10 +1610,15 @@ pub const Lowering = struct { const str = self.builder.constString(sid); break :blk self.builder.boxAny(str, .string); } - break :blk self.emitPlaceholder(te.name); + // Type expressions are always type names from the parser — they appear + // in value position when used as args to cast() etc. Silent placeholder. + if (self.isKnownTypeName(te.name)) { + break :blk self.emitPlaceholder(te.name); + } + break :blk self.emitError(te.name, node.span); }, - else => self.emitPlaceholder("unknown_expr"), + else => self.emitError("unknown_expr", node.span), }; } @@ -1766,7 +1778,7 @@ pub const Lowering = struct { .bit_xor => self.builder.emit(.{ .bit_xor = .{ .lhs = lhs, .rhs = rhs } }, ty), .shl => self.builder.emit(.{ .shl = .{ .lhs = lhs, .rhs = rhs } }, ty), .shr => self.builder.emit(.{ .shr = .{ .lhs = lhs, .rhs = rhs } }, ty), - .in_op => self.emitPlaceholder("in_op"), + .in_op => self.emitError("in_op", bop.lhs.span), }; } @@ -1955,7 +1967,12 @@ pub const Lowering = struct { } // Optional binding: `if val := expr { ... }` + // Clear target_type so the ternary's result type doesn't leak into the condition + // (e.g., `if x != 0 then 1.0 else 2.0` — the `0` must be s64, not f32) + const saved_cond_target = self.target_type; + self.target_type = null; const opt_val = self.lowerExpr(ie.condition); + self.target_type = saved_cond_target; const cond = if (ie.binding_name != null) blk: { // The condition is an optional — emit has_value check break :blk self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool); @@ -3552,6 +3569,28 @@ pub const Lowering = struct { if (hasComptimeParams(fd)) { return self.lowerComptimeCall(fd, c); } + // Early detection of generic function calls — skip arg lowering for type params + // because lowerGenericCall resolves type params from AST nodes, not lowered refs. + // Only if the name is NOT shadowed by a local variable (closure, fn ptr, etc.) + const shadowed = if (self.scope) |scope| scope.lookup(c.callee.data.identifier.name) != null else false; + if (fd.type_params.len > 0 and !shadowed) { + // Types are explicit when call args match param count (e.g., are_equal(Point, p1, p2)) + // Types are inferred when call args < param count (e.g., are_equal(p1, p2)) + const types_explicit = c.args.len == fd.params.len; + var lowered_args = std.ArrayList(Ref).empty; + defer lowered_args.deinit(self.alloc); + for (c.args, 0..) |arg, ai| { + // Skip type param args only when types are passed explicitly + if (types_explicit and ai < fd.params.len and isTypeParamDecl(&fd.params[ai], fd.type_params)) { + lowered_args.append(self.alloc, Ref.none) catch unreachable; + } else { + const saved_target = self.target_type; + lowered_args.append(self.alloc, self.lowerExpr(arg)) catch unreachable; + self.target_type = saved_target; + } + } + return self.lowerGenericCall(fd, early_name, c, lowered_args.items); + } } } @@ -3576,6 +3615,11 @@ pub const Lowering = struct { } } for (c.args, 0..) |arg, ai| { + // Skip spread expressions — they'll be handled by packVariadicCallArgs from AST + if (arg.data == .spread_expr) { + args.append(self.alloc, Ref.none) catch unreachable; + continue; + } const saved_target = self.target_type; if (ai < param_types.len) { self.target_type = param_types[ai]; @@ -3778,8 +3822,8 @@ pub const Lowering = struct { } } } - // Unresolved — emit placeholder - return self.emitPlaceholder(id.name); + // Unresolved function call + return self.emitError(id.name, c.callee.span); }, .field_access => |fa| { // Pattern-match context.allocator.alloc/dealloc → heap_alloc/heap_free @@ -3791,6 +3835,30 @@ pub const Lowering = struct { if (inner_call.callee.data == .identifier) { const inner_name = inner_call.callee.data.identifier.name; const resolved = if (self.scope) |scope| (scope.lookupFn(inner_name) orelse inner_name) else inner_name; + + // Generic struct static method: Animated(Size).make(...) + if (self.struct_template_map.getPtr(resolved)) |tmpl| { + const inst_ty = self.instantiateGenericStruct(tmpl, inner_call.args); + const inst_name = self.formatTypeName(inst_ty); + // Look up template method, monomorphize, and call + if (self.struct_instance_template.get(inst_name)) |tmpl_name| { + const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, fa.field }) catch fa.field; + if (self.fn_ast_map.get(tmpl_qualified)) |fd| { + if (self.struct_instance_bindings.getPtr(inst_name)) |bindings| { + const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ inst_name, fa.field }) catch fa.field; + if (!self.lowered_functions.contains(mangled)) { + self.monomorphizeFunction(fd, mangled, bindings); + } + if (self.resolveFuncByName(mangled)) |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + self.coerceCallArgs(args.items, func.params); + return self.builder.call(fid, args.items, func.ret); + } + } + } + } + } + if (self.fn_ast_map.get(resolved)) |fd| { if (fd.type_params.len > 0) { // Try instantiate as type function @@ -3908,7 +3976,7 @@ pub const Lowering = struct { } } } - return self.emitPlaceholder(func_name); + return self.emitError(func_name, c.callee.span); } // Method call: obj.method(args) → prepend obj (or &obj for *Self receivers) @@ -4017,7 +4085,7 @@ pub const Lowering = struct { const ret_ty = self.module.functions.items[@intFromEnum(fid)].ret; return self.builder.call(fid, method_args.items, ret_ty); } - return self.emitPlaceholder(fa.field); + return self.emitError(fa.field, c.callee.span); }, .enum_literal => |el| { const target = self.target_type orelse .s64; @@ -4803,7 +4871,7 @@ pub const Lowering = struct { self.builder.store(ptr, store_val); }, else => { - _ = self.emitPlaceholder("multi_assign_target"); + _ = self.emitError("multi_assign_target", target.span); }, } } @@ -5428,7 +5496,7 @@ pub const Lowering = struct { return self.builder.call(fid, value_args.items, ret_ty); } - return self.emitPlaceholder(base_name); + return self.emitError(base_name, call_node.callee.span); } /// Create a monomorphized instance of a generic function. @@ -5479,8 +5547,8 @@ pub const Lowering = struct { } // Lower the type tag (runtime value) and Any value BEFORE the switch - const type_tag = self.lowerExpr(type_tag_node orelse return self.emitPlaceholder("dispatch")); - const any_val = self.lowerExpr(any_val_node orelse return self.emitPlaceholder("dispatch")); + const type_tag = self.lowerExpr(type_tag_node orelse return self.emitError("dispatch", call_node.callee.span)); + const any_val = self.lowerExpr(any_val_node orelse return self.emitError("dispatch", call_node.callee.span)); // Lower non-cast arguments once (before the switch) var other_args = std.ArrayList(?Ref).empty; @@ -6945,10 +7013,15 @@ pub const Lowering = struct { for (sd.type_params, 0..) |tp, i| { tps[i] = .{ .name = self.alloc.dupe(u8, tp.name) catch return, - .is_type_param = if (tp.constraint.data == .type_expr) - std.mem.eql(u8, tp.constraint.data.type_expr.name, "Type") - else - false, + // $T: Type, $T: Lerpable, $T: Type/Eq — all are type params + // Only value params like $N: u32 are non-type + .is_type_param = if (tp.constraint.data == .type_expr) blk: { + const cname = tp.constraint.data.type_expr.name; + // "Type" or a protocol name → type param + break :blk std.mem.eql(u8, cname, "Type") or + self.protocol_decl_map.contains(cname) or + self.protocol_ast_map.contains(cname); + } else false, }; } @@ -7363,7 +7436,20 @@ pub const Lowering = struct { var call_args = std.ArrayList(Ref).empty; defer call_args.deinit(self.alloc); // Pass ctx (ref 0) as first arg (it's the concrete *Type disguised as *void) - call_args.append(self.alloc, Ref.fromIndex(0)) catch unreachable; + // If the concrete method expects a value (e.g., f32) not a pointer, load from ctx + const ctx_ref = Ref.fromIndex(0); + if (concrete_func.params.len > 0) { + const first_concrete_ty = concrete_func.params[0].ty; + const first_info = self.module.types.get(first_concrete_ty); + if (first_info != .pointer) { + // Concrete expects value — load from ctx pointer + call_args.append(self.alloc, self.builder.load(ctx_ref, first_concrete_ty)) catch unreachable; + } else { + call_args.append(self.alloc, ctx_ref) catch unreachable; + } + } else { + call_args.append(self.alloc, ctx_ref) catch unreachable; + } for (method.param_types, 0..) |proto_pty, i| { var arg_ref = Ref.fromIndex(@intCast(i + 1)); // If protocol param is a pointer (Self→*void) but concrete method @@ -7380,9 +7466,20 @@ pub const Lowering = struct { call_args.append(self.alloc, arg_ref) catch unreachable; } const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable; - const result = self.builder.call(concrete_fid, owned_args, method.ret_type); + const concrete_ret = concrete_func.ret; + const result = self.builder.call(concrete_fid, owned_args, concrete_ret); if (method.ret_type != .void) { - self.builder.ret(result, method.ret_type); + // If protocol returns *void (Self) but concrete returns a value type, + // box the value: alloca+store and return the pointer + const ret_info = self.module.types.get(method.ret_type); + const concrete_ret_info = self.module.types.get(concrete_ret); + if (ret_info == .pointer and concrete_ret_info != .pointer) { + const slot = self.builder.alloca(concrete_ret); + self.builder.store(slot, result); + self.builder.ret(slot, method.ret_type); + } else { + self.builder.ret(result, method.ret_type); + } } else { self.builder.retVoid(); } @@ -7459,7 +7556,7 @@ pub const Lowering = struct { break; } } - const mi = method_info orelse return self.emitPlaceholder(method_name); + const mi = method_info orelse return self.emitError(method_name, null); const midx = method_idx orelse 0; // Extract ctx from protocol struct (field 0) @@ -7473,7 +7570,7 @@ pub const Lowering = struct { } else blk: { // Vtable: load vtable struct, extract fn_ptr at method_idx const vtable_ptr = self.builder.structGet(receiver, 1, void_ptr); - const vtable_ty = self.protocol_vtable_type_map.get(proto_info.name) orelse return self.emitPlaceholder("vtable"); + const vtable_ty = self.protocol_vtable_type_map.get(proto_info.name) orelse return self.emitError("vtable", null); const vtable = self.builder.emit(.{ .deref = .{ .operand = vtable_ptr } }, vtable_ty); break :blk self.builder.structGet(vtable, @intCast(midx), void_ptr); }; @@ -7489,9 +7586,9 @@ pub const Lowering = struct { for (args, 0..) |a, i| { const expected_ty = if (i < mi.param_types.len) mi.param_types[i] else void_ptr; const arg_ty = self.builder.getRefType(a); - // If protocol method expects *void but we have a struct value (not a pointer), convert to pointer + // If protocol method expects *void but we have a value (struct or primitive), convert to pointer const is_pointer_ty = if (!arg_ty.isBuiltin()) self.module.types.get(arg_ty) == .pointer else false; - if (expected_ty == void_ptr and arg_ty != void_ptr and !arg_ty.isBuiltin() and !is_pointer_ty) { + if (expected_ty == void_ptr and arg_ty != void_ptr and !is_pointer_ty) { const slot = self.builder.alloca(arg_ty); self.builder.store(slot, a); call_args.append(self.alloc, slot) catch unreachable; @@ -7502,7 +7599,22 @@ pub const Lowering = struct { } } const owned = self.alloc.dupe(Ref, call_args.items) catch unreachable; - return self.builder.emit(.{ .call_indirect = .{ .callee = fn_ptr, .args = owned } }, mi.ret_type); + const raw_result = self.builder.emit(.{ .call_indirect = .{ .callee = fn_ptr, .args = owned } }, mi.ret_type); + + // If protocol method returns *void (Self) and the caller expects a value type, + // unbox: load the concrete value from the returned pointer + if (mi.ret_type != .void) { + const ret_info = self.module.types.get(mi.ret_type); + if (ret_info == .pointer) { + if (self.target_type) |target| { + const target_info = self.module.types.get(target); + if (target_info != .pointer) { + return self.builder.load(raw_result, target); + } + } + } + } + return raw_result; } /// Resolve the concrete type name for protocol erasure. @@ -8163,6 +8275,24 @@ pub const Lowering = struct { return self.builder.emit(.{ .placeholder = sid }, .s64); } + /// Check if a name refers to a known type (primitive or registered struct/enum/union). + /// Used to distinguish type-as-value (silent placeholder) from genuinely unresolved names. + fn isKnownTypeName(self: *Lowering, name: []const u8) bool { + if (type_bridge.resolveTypePrimitive(name) != null) return true; + if (self.type_bindings) |bindings| { + if (bindings.get(name) != null) return true; + } + const name_id = self.module.types.internString(name); + return self.module.types.findByName(name_id) != null; + } + + fn emitError(self: *Lowering, name: []const u8, span: ?ast.Span) Ref { + if (self.diagnostics) |diags| { + diags.addFmt(.err, span, "unresolved: '{s}'", .{name}); + } + return self.emitPlaceholder(name); + } + /// Insert a conversion if src_ty and dst_ty differ. /// Handles int widening/narrowing, float widening/narrowing, and int↔float. fn coerceToType(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref { @@ -8188,6 +8318,14 @@ pub const Lowering = struct { } } + // void → Optional: produce null (void is the type of null_literal) + if (src_ty == .void and !dst_ty.isBuiltin()) { + const dst_info = self.module.types.get(dst_ty); + if (dst_info == .optional) { + return self.builder.constNull(dst_ty); + } + } + // Concrete → Optional wrapping if (!dst_ty.isBuiltin()) { const dst_info = self.module.types.get(dst_ty); diff --git a/src/main.zig b/src/main.zig index 511705b..e859250 100644 --- a/src/main.zig +++ b/src/main.zig @@ -367,7 +367,7 @@ fn dumpSxIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8) !v comp.parse() catch { comp.renderErrors(); return error.CompileError; }; comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; }; - var ir_module = comp.lowerToIR(); + var ir_module = comp.lowerToIR() catch { comp.renderErrors(); return error.CompileError; }; defer ir_module.deinit(); var aw = std.Io.Writer.Allocating.init(allocator); diff --git a/src/parser.zig b/src/parser.zig index 132b2bf..c8f932e 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -964,8 +964,8 @@ pub const Parser = struct { } self.advance(); - // Target type name - if (self.current.tag != .identifier) { + // Target type name (identifiers like s64, or keywords like f32/f64) + if (self.current.tag != .identifier and !self.current.tag.isTypeKeyword()) { return self.fail("expected type name after 'for'"); } const target_type = self.tokenSlice(self.current);