metal: GPU protocol + MetalGPU renders MSL triangle on iOS

Phase 8 step 3a of the Metal renderer port:

- New library/modules/gpu/ with types.sx (handles + ClearColor +
  TextureFormat enum), api.sx (GPU :: protocol { ... } covering the
  lifecycle / per-frame / resource / per-draw surface), and metal.sx
  (MetalGPU backend implementing the protocol against CAMetalLayer).
  Resource handles are 1-based indices into backend List(*void) tables.
  MTL aggregates >16 bytes (MTLRegion, MTLScissorRect) pass via *T to
  match arm64 Apple's indirect-by-reference ABI; MTLClearColor + CGSize
  go through the HFA path as direct fn-pointer casts on objc_msgSend.

- UIKitPlatform got a gpu_mode: GpuMode toggle + sibling SxMetalView
  class registration. In metal mode init skips EAGL context, the
  did_finish_launching IMP skips the EAGL drawable-properties dict,
  layoutSubviews reads the layer's bounds * dpi_scale into pixel_w/h
  instead of allocating a GL renderbuffer, and end_frame is a no-op
  (the MetalGPU owns its own present).

- examples/63-metal-clear.sx verifies the pipeline end-to-end on iOS
  sim — compiles a pass-through MSL shader (packed_float2/packed_float4
  to avoid alignment padding), uploads 3 vertices, draws a colored
  triangle on a dark-blue clear.

Compiler fixes (filed-and-fixed in this branch):

- inline if X { return E; } followed by a fall-through final expression
  no longer emits two terminators into the same basic block. Verified
  by examples/83-inline-if-return-fallthrough.sx.

- Top-level type alias Name :: u32; now resolves correctly as the type
  annotation on a global variable (was treated as ptr {}, breaking
  comparisons + initializers). Verified by examples/84-global-type-alias.sx.

Issue->feature promotion:

- 16 historical examples/issue-NNNN.sx repros now confirmed-fixed and
  renamed to focused feature names (67-82). Each gains a
  tests/expected/*.txt + .exit pair so the regression suite covers them.

- 5 stale issue repros deleted (subsumed by broader tests).

Regression suite: 68 passing, 0 failed. macOS chess builds + runs; wasm
chess builds; iOS sim GLES chess still renders the full board; iOS sim
Metal demo renders the triangle.
This commit is contained in:
agra
2026-05-17 19:36:37 +03:00
parent 2ff24e29cc
commit a938c4f900
66 changed files with 1248 additions and 376 deletions

View File

@@ -2848,19 +2848,29 @@ pub const LLVMEmitter = struct {
}
fn emitConstAggregate(self: *LLVMEmitter, agg: []const ir_inst.ConstantValue, llvm_ty: c.LLVMTypeRef) c.LLVMValueRef {
const elem_ty = c.LLVMGetElementType(llvm_ty);
const kind = c.LLVMGetTypeKind(llvm_ty);
const is_struct = kind == c.LLVMStructTypeKind;
const n: c_uint = @intCast(agg.len);
const vals = self.alloc.alloc(c.LLVMValueRef, agg.len) catch return c.LLVMConstNull(llvm_ty);
defer self.alloc.free(vals);
for (agg, 0..) |cv, i| {
const elem_ty = if (is_struct)
c.LLVMStructGetTypeAtIndex(llvm_ty, @intCast(i))
else
c.LLVMGetElementType(llvm_ty);
vals[i] = switch (cv) {
.int => |v| c.LLVMConstInt(elem_ty, @bitCast(v), 1),
.float => |v| c.LLVMConstReal(elem_ty, v),
.boolean => |v| c.LLVMConstInt(elem_ty, @intFromBool(v), 0),
.string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)),
.aggregate => |inner| self.emitConstAggregate(inner, elem_ty),
else => c.LLVMConstNull(elem_ty),
};
}
if (is_struct) {
return c.LLVMConstNamedStruct(llvm_ty, vals.ptr, n);
}
const elem_ty = c.LLVMGetElementType(llvm_ty);
return c.LLVMConstArray(elem_ty, vals.ptr, n);
}

View File

@@ -447,10 +447,9 @@ pub const Lowering = struct {
},
.var_decl => |vd| {
// Top-level mutable global (e.g., `context : Context = ---;`)
const var_ty = if (vd.type_annotation) |ta|
type_bridge.resolveAstType(ta, &self.module.types)
else
.s64;
// Use self.resolveType so type aliases like `Handle :: u32;` resolve
// to their target type (not a synthetic empty struct).
const var_ty = self.resolveType(vd.type_annotation);
const name_id = self.module.types.internString(vd.name);
const init_val: ?inst_mod.ConstantValue = if (vd.value) |v| switch (v.data) {
.undef_literal => .zeroinit,
@@ -459,6 +458,7 @@ pub const Lowering = struct {
.float_literal => |fl| .{ .float = fl.value },
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
.array_literal => |al| self.constArrayLiteral(al.elements),
.struct_literal => |sl| self.constStructLiteral(&sl, var_ty),
else => null,
} else null;
const gid = self.module.addGlobal(.{
@@ -479,22 +479,67 @@ pub const Lowering = struct {
fn constArrayLiteral(self: *Lowering, elements: []const *const Node) ?inst_mod.ConstantValue {
const vals = self.alloc.alloc(inst_mod.ConstantValue, elements.len) catch return null;
for (elements, 0..) |elem, i| {
vals[i] = switch (elem.data) {
.int_literal => |il| .{ .int = il.value },
.bool_literal => |bl| .{ .boolean = bl.value },
.float_literal => |fl| .{ .float = fl.value },
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
.unary_op => |uo| switch (uo.op) {
.negate => switch (uo.operand.data) {
.int_literal => |il| .{ .int = -il.value },
.float_literal => |fl| .{ .float = -fl.value },
else => return null,
},
else => return null,
vals[i] = self.constExprValue(elem) orelse return null;
}
return .{ .aggregate = vals };
}
/// Try to convert a single AST expression into a compile-time ConstantValue.
/// Returns null if the expression is not constant-foldable here.
fn constExprValue(self: *Lowering, expr: *const Node) ?inst_mod.ConstantValue {
return switch (expr.data) {
.int_literal => |il| .{ .int = il.value },
.bool_literal => |bl| .{ .boolean = bl.value },
.float_literal => |fl| .{ .float = fl.value },
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
.undef_literal => .zeroinit,
.unary_op => |uo| switch (uo.op) {
.negate => switch (uo.operand.data) {
.int_literal => |il| .{ .int = -il.value },
.float_literal => |fl| .{ .float = -fl.value },
else => null,
},
.array_literal => |al| self.constArrayLiteral(al.elements) orelse return null,
else => return null,
else => null,
},
.array_literal => |al| self.constArrayLiteral(al.elements),
else => null,
};
}
/// Try to convert a struct literal into a compile-time ConstantValue.aggregate of the
/// struct's fields in declaration order, filling missing fields from the struct's
/// field defaults. Returns null if any value is not constant-foldable.
fn constStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, ty: TypeId) ?inst_mod.ConstantValue {
if (ty.isBuiltin()) return null;
const ti = self.module.types.get(ty);
if (ti != .@"struct") return null;
const struct_fields = ti.@"struct".fields;
const struct_name = self.module.types.getString(ti.@"struct".name);
const field_defaults: []const ?*const Node = self.struct_defaults_map.get(struct_name) orelse &.{};
const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null;
const vals = self.alloc.alloc(inst_mod.ConstantValue, struct_fields.len) catch return null;
for (struct_fields, 0..) |sf, fi| {
const sf_name = self.module.types.getString(sf.name);
const init_expr: ?*const Node = blk: {
if (has_names) {
for (sl.field_inits) |init_pair| {
if (init_pair.name) |n| {
if (std.mem.eql(u8, n, sf_name)) break :blk init_pair.value;
}
}
} else if (fi < sl.field_inits.len) {
break :blk sl.field_inits[fi].value;
}
if (fi < field_defaults.len) break :blk field_defaults[fi];
break :blk null;
};
if (init_expr) |e| {
vals[fi] = self.constExprValue(e) orelse return null;
} else {
vals[fi] = .zeroinit;
}
}
return .{ .aggregate = vals };
}
@@ -862,6 +907,12 @@ pub const Lowering = struct {
fn lowerInlineBranch(self: *Lowering, node: *const Node) Ref {
if (node.data == .block) {
self.lowerBlock(node);
// A `return` inside the branch terminates the current LLVM block; propagate
// that up so the enclosing block lowering stops emitting fall-through.
if (self.currentBlockHasTerminator()) {
self.block_terminated = true;
return .none;
}
return self.builder.constInt(0, .void);
}
return self.lowerExpr(node);
@@ -1152,12 +1203,13 @@ pub const Lowering = struct {
const elem_ty = self.getElementType(self.inferExprType(asgn.target.data.index_expr.object));
if (elem_ty != .void) self.target_type = elem_ty;
} else if (asgn.target.data == .field_access) {
// For obj.field = val, set target_type to the field's type
// Only for enum literals and struct literals — these need target_type to resolve.
// Avoid setting it for complex RHS expressions (calls, casts) because
// resolveCallParamTypes can't override target_type for method-call args.
// For obj.field = val, set target_type to the field's type so RHS
// sub-expressions (enum/struct literals, branch arms, xx casts) can
// resolve against it. Skipped for forms that would forward the type
// unchanged into method-call arg slots (`resolveCallParamTypes` can't
// override target_type per-arg).
const needs_target = switch (asgn.value.data) {
.enum_literal, .struct_literal => true,
.enum_literal, .struct_literal, .if_expr, .match_expr, .block, .unary_op, .binary_op => true,
.call => |vc| vc.callee.data == .enum_literal,
else => false,
};
@@ -1266,7 +1318,7 @@ pub const Lowering = struct {
for (fields, 0..) |f, i| {
if (f.name == field_name_id) {
const gep = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty));
const src_ty = self.inferExprType(asgn.value);
const src_ty = self.builder.getRefType(val);
const coerced = self.coerceToType(val, src_ty, f.ty);
self.storeOrCompound(gep, coerced, asgn.op, f.ty);
return;
@@ -1280,7 +1332,7 @@ pub const Lowering = struct {
// GEP into union payload area, then into the struct field
const union_gep = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty));
const field_gep = self.builder.structGepTyped(union_gep, @intCast(si), sf.ty, f.ty);
const src_ty = self.inferExprType(asgn.value);
const src_ty = self.builder.getRefType(val);
const coerced = self.coerceToType(val, src_ty, sf.ty);
self.storeOrCompound(field_gep, coerced, asgn.op, sf.ty);
return;
@@ -1308,8 +1360,9 @@ pub const Lowering = struct {
// target type, storing element-sized bytes instead of a pointer.
const gep_ty = self.module.types.ptrTo(field_ty);
const gep = self.builder.structGepTyped(obj_ptr, field_idx, gep_ty, obj_ty);
// Coerce value to field type
const src_ty = self.inferExprType(asgn.value);
// Coerce value to field type — use the lowered value's actual type
// (not inferExprType, which can re-read target_type after restore).
const src_ty = self.builder.getRefType(val);
const coerced = self.coerceToType(val, src_ty, field_ty);
self.storeOrCompound(gep, coerced, asgn.op, field_ty);
}