This commit is contained in:
agra
2026-03-03 16:18:58 +02:00
parent 23f444033a
commit 0336f361c7
7 changed files with 181 additions and 33 deletions

View File

@@ -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";
// ============================================================

View File

@@ -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;

5
examples/modules/test.sx Normal file
View File

@@ -0,0 +1,5 @@
assert :: (condition: bool) {
if !condition {
out("assertion failed\n");
}
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);