iOS lock step keyboard + metal

This commit is contained in:
agra
2026-05-18 17:40:10 +03:00
parent b43472e6ab
commit f9ecf9d00e
68 changed files with 4794 additions and 203 deletions

View File

@@ -83,6 +83,7 @@ pub const Lowering = struct {
local_fn_counter: u32 = 0, // unique counter for mangling local function names
import_flags: std.StringHashMap(bool), // tracks whether each function is imported
module_scopes: ?*std.StringHashMap(std.StringHashMap(void)) = null, // per-module visible names (from import resolution)
import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null, // module path → set of directly imported paths (used by param_impl_map visibility filter)
current_source_file: ?[]const u8 = null, // source file of function currently being lowered
type_bindings: ?std.StringHashMap(TypeId) = null, // generic type param bindings ($T → concrete TypeId)
current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch)
@@ -103,6 +104,7 @@ pub const Lowering = struct {
protocol_thunk_map: std.StringHashMap([]const FuncId) = std.StringHashMap([]const FuncId).init(std.heap.page_allocator), // "Proto\x00Type" → thunk FuncIds
protocol_vtable_type_map: std.StringHashMap(TypeId) = std.StringHashMap(TypeId).init(std.heap.page_allocator), // protocol name → vtable struct TypeId
protocol_vtable_global_map: std.StringHashMap(inst_mod.GlobalId) = std.StringHashMap(inst_mod.GlobalId).init(std.heap.page_allocator), // "Proto\x00Type" → vtable GlobalId
param_impl_map: std.StringHashMap(std.ArrayList(ParamImplEntry)) = std.StringHashMap(std.ArrayList(ParamImplEntry)).init(std.heap.page_allocator), // "Proto\x00<arg_mangled>\x00<src_mangled>" → impl entries (parameterised protocols only; list lets Phase 4/5 detect cross-module overlap)
struct_const_map: std.StringHashMap(StructConstInfo) = std.StringHashMap(StructConstInfo).init(std.heap.page_allocator), // "Struct.CONST" → value info
module_const_map: std.StringHashMap(ModuleConstInfo) = std.StringHashMap(ModuleConstInfo).init(std.heap.page_allocator), // module-level value constants (e.g. AF_INET :s32: 2)
foreign_name_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // sx name → C name for #foreign renames
@@ -111,6 +113,7 @@ pub const Lowering = struct {
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
xx_reentrancy: std.AutoHashMap(u64, void) = std.AutoHashMap(u64, void).init(std.heap.page_allocator), // (src_ty, dst_ty) pairs currently being resolved through user-space Into; prevents infinite monomorphisation when a convert body re-enters the same xx
pub const ComptimeValue = union(enum) {
int_val: i64,
@@ -139,6 +142,17 @@ pub const Lowering = struct {
ret_type: TypeId,
};
/// One impl block for a parameterised protocol (e.g. `impl Into(Block) for Closure() -> void`).
/// Stored in `param_impl_map` keyed by (protocol_name, target_args_mangled, source_mangled).
/// `defining_module` enables import-scoped visibility + cross-module duplicate diagnostics.
const ParamImplEntry = struct {
methods: []const *const ast.FnDecl,
source_ty: TypeId,
target_args: []const TypeId,
defining_module: []const u8,
span: ast.Span,
};
/// Owned copy of a generic struct template (AST pointers are copied/interned to survive imports)
const StructTemplate = struct {
name: []const u8,
@@ -294,11 +308,11 @@ pub const Lowering = struct {
.union_decl => {
_ = type_bridge.resolveAstType(decl, &self.module.types);
},
.protocol_decl => |pd| {
self.registerProtocolDecl(&pd);
.protocol_decl => {
self.registerProtocolDecl(&decl.data.protocol_decl);
},
.impl_block => |ib| {
self.registerImplBlock(&ib, is_imported);
.impl_block => {
self.registerImplBlock(&decl.data.impl_block, is_imported, decl);
},
.namespace_decl => |ns| {
if (self.main_file != null) {
@@ -431,11 +445,11 @@ pub const Lowering = struct {
// Register plain union types in the type table
_ = type_bridge.resolveAstType(decl, &self.module.types);
},
.protocol_decl => |pd| {
self.registerProtocolDecl(&pd);
.protocol_decl => {
self.registerProtocolDecl(&decl.data.protocol_decl);
},
.impl_block => |ib| {
self.registerImplBlock(&ib, is_imported);
.impl_block => {
self.registerImplBlock(&decl.data.impl_block, is_imported, decl);
},
.namespace_decl => |ns| {
if (self.main_file != null) {
@@ -450,8 +464,12 @@ pub const Lowering = struct {
// 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) {
// Foreign globals reference a symbol defined in libSystem etc.
// (`_NSConcreteStackBlock : *void #foreign;`). The C symbol
// name is the optional override or the sx name itself.
const sym_name = vd.foreign_name orelse vd.name;
const name_id = self.module.types.internString(sym_name);
const init_val: ?inst_mod.ConstantValue = if (vd.is_foreign) null else if (vd.value) |v| switch (v.data) {
.undef_literal => .zeroinit,
.int_literal => |il| .{ .int = il.value },
.bool_literal => |bl| .{ .boolean = bl.value },
@@ -466,6 +484,7 @@ pub const Lowering = struct {
.ty = var_ty,
.init_val = init_val,
.is_const = false,
.is_extern = vd.is_foreign,
});
self.global_names.put(vd.name, .{ .id = gid, .ty = var_ty }) catch {};
},
@@ -5976,14 +5995,19 @@ pub const Lowering = struct {
const saved_bindings = self.type_bindings;
const saved_defer_base = self.func_defer_base;
const saved_block_terminated = self.block_terminated;
const saved_target = self.target_type;
self.func_defer_base = self.defer_stack.items.len;
self.block_terminated = false;
// Install type bindings
self.type_bindings = bindings.*;
// Resolve return type with type bindings active
// Resolve return type with type bindings active. The body's tail
// expression inherits this as its target_type so bare `.{...}`
// literals resolve to the monomorphised return type instead of
// whatever leaked in from the caller (e.g. caller's xx target).
const ret_ty = self.resolveReturnType(fd);
self.target_type = ret_ty;
// Build param list (substituting type params, skipping type param declarations)
var params = std.ArrayList(Function.Param).empty;
@@ -6060,6 +6084,7 @@ pub const Lowering = struct {
self.scope = saved_scope;
self.func_defer_base = saved_defer_base;
self.block_terminated = saved_block_terminated;
self.target_type = saved_target;
self.builder.func = saved_func;
self.builder.current_block = saved_block;
self.builder.inst_counter = saved_counter;
@@ -6435,10 +6460,74 @@ pub const Lowering = struct {
const inner = self.mangleTypeName(v.element);
break :blk std.fmt.allocPrint(self.alloc, "vec_{d}_{s}", .{ v.length, inner }) catch "vector";
},
.closure => |c| self.mangleParamList("cl", c.params, c.ret),
.function => |f| self.mangleParamList("fn", f.params, f.ret),
.tuple => |t| blk: {
var buf = std.ArrayList(u8).empty;
buf.appendSlice(self.alloc, "tu") catch break :blk "tuple";
for (t.fields) |fid| {
buf.append(self.alloc, '_') catch break :blk "tuple";
buf.appendSlice(self.alloc, self.mangleTypeName(fid)) catch break :blk "tuple";
}
break :blk buf.items;
},
else => @tagName(info),
};
}
/// Collect impl entries visible from `current_source_file` — defined in
/// the current file or in any module the current file transitively
/// imports. Falls open (returns all entries) when the source-file
/// context or import graph isn't wired (e.g. comptime callers).
fn findVisibleImpls(self: *Lowering, entries: []const ParamImplEntry, out: *std.ArrayList(ParamImplEntry)) void {
const here = self.current_source_file orelse {
out.appendSlice(self.alloc, entries) catch {};
return;
};
const graph = self.import_graph orelse {
out.appendSlice(self.alloc, entries) catch {};
return;
};
// BFS over the import graph to compute the visible set.
var visible = std.StringHashMap(void).init(self.alloc);
defer visible.deinit();
visible.put(here, {}) catch {};
var queue = std.ArrayList([]const u8).empty;
defer queue.deinit(self.alloc);
queue.append(self.alloc, here) catch {};
var head: usize = 0;
while (head < queue.items.len) : (head += 1) {
const node = queue.items[head];
const direct = graph.get(node) orelse continue;
var it = direct.iterator();
while (it.next()) |kv| {
const next = kv.key_ptr.*;
if (visible.contains(next)) continue;
visible.put(next, {}) catch {};
queue.append(self.alloc, next) catch {};
}
}
for (entries) |e| {
if (visible.contains(e.defining_module)) {
out.append(self.alloc, e) catch {};
}
}
}
fn mangleParamList(self: *Lowering, prefix: []const u8, params: []const TypeId, ret: TypeId) []const u8 {
var buf = std.ArrayList(u8).empty;
buf.appendSlice(self.alloc, prefix) catch return prefix;
for (params) |p| {
buf.append(self.alloc, '_') catch return prefix;
buf.appendSlice(self.alloc, self.mangleTypeName(p)) catch return prefix;
}
buf.appendSlice(self.alloc, "__") catch return prefix;
buf.appendSlice(self.alloc, self.mangleTypeName(ret)) catch return prefix;
return buf.items;
}
/// Resolve type category names (like "int", "struct", "float") to matching TypeId tag values.
/// Returns a list of TypeId index values that match the category.
fn resolveTypeCategoryTags(self: *Lowering, name: []const u8) []const u64 {
@@ -6584,6 +6673,27 @@ pub const Lowering = struct {
if (c.callee.data == .field_access) {
const fa = c.callee.data.field_access;
const obj_ty = self.inferExprType(fa.object);
// Protocol-typed receiver: look up the method on the protocol decl. The
// protocol's ProtocolMethodInfo.param_types already excludes self.
if (self.getProtocolInfo(obj_ty)) |proto_info| {
for (proto_info.methods) |m| {
if (std.mem.eql(u8, m.name, fa.field)) return m.param_types;
}
}
// Closure-typed struct field: `c.on(args)` lowers to call_closure on
// the field value. Pick up the callee's param types from the closure
// type so each arg gets the right target_type during lowering.
if (!obj_ty.isBuiltin()) {
const field_name_id = self.module.types.internString(fa.field);
const struct_fields = self.getStructFields(obj_ty);
for (struct_fields) |f| {
if (f.name == field_name_id and !f.ty.isBuiltin()) {
const fti = self.module.types.get(f.ty);
if (fti == .closure) return fti.closure.params;
if (fti == .function) return fti.function.params;
}
}
}
if (self.getStructTypeName(obj_ty)) |sname| {
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch return &.{};
// Try already-lowered functions first
@@ -7498,6 +7608,16 @@ pub const Lowering = struct {
/// Non-inline protocols: { ctx: *void, __vtable: *void }
/// Also stores protocol info for dispatch and vtable struct type for vtable protocols.
fn registerProtocolDecl(self: *Lowering, pd: *const ast.ProtocolDecl) void {
// Parameterised protocols are compile-time-only — no vtable, no boxed
// instance struct. Methods reference unbound type params (e.g.
// `convert :: () -> Target`) that only get a concrete TypeId per
// (Source, Target) pair at xx resolution time. Stash the AST so
// `param_impl_map` lookup can resolve method signatures lazily.
if (pd.type_params.len > 0) {
self.protocol_ast_map.put(pd.name, pd) catch {};
return;
}
const table = &self.module.types;
const name_id = table.internString(pd.name);
@@ -7596,7 +7716,15 @@ pub const Lowering = struct {
}
/// Register an impl block: register its methods as TypeName.method in fn_ast_map.
fn registerImplBlock(self: *Lowering, ib: *const ast.ImplBlock, is_imported: bool) void {
fn registerImplBlock(self: *Lowering, ib: *const ast.ImplBlock, is_imported: bool, decl: *const Node) void {
// Parameterised-protocol impl (e.g. `impl Into(Block) for Closure() -> void`):
// record into `param_impl_map` for compile-time resolution by `lowerXX`.
// Methods are NOT registered in fn_ast_map — they're monomorphised lazily
// per (Source, Target) pair at the xx call site.
if (ib.protocol_type_args.len > 0) {
self.registerParamImpl(ib, decl);
return;
}
// Collect explicitly implemented method names
var impl_methods = std.StringHashMap(void).init(self.alloc);
defer impl_methods.deinit();
@@ -7625,6 +7753,80 @@ pub const Lowering = struct {
}
}
/// Register a parameterised-protocol impl into `param_impl_map`.
/// Resolves the protocol's type args + the source type, mangles them, and
/// stashes the impl's method fn_decls for later monomorphisation by
/// `lowerXX`. Same-module duplicate impls produce a diagnostic here;
/// cross-module duplicates are detected at the xx resolution site.
fn registerParamImpl(self: *Lowering, ib: *const ast.ImplBlock, decl: *const Node) void {
const table = &self.module.types;
// Resolve the protocol's type-arg list to concrete TypeIds.
var arg_tys = std.ArrayList(TypeId).empty;
for (ib.protocol_type_args) |arg_node| {
const t = type_bridge.resolveAstType(arg_node, table);
arg_tys.append(self.alloc, t) catch return;
}
// Resolve the source type. Parser stores it on `target_type_expr` for
// parameterised impls (back-compat `target_type` string is kept for
// simple cases but the canonical form is the TypeExpr).
const src_ty: TypeId = if (ib.target_type_expr) |te|
type_bridge.resolveAstType(te, table)
else if (ib.target_type.len > 0)
type_bridge.resolveAstType(&.{ .span = decl.span, .data = .{ .type_expr = .{ .name = ib.target_type } } }, table)
else
return;
// Mangle into the lookup key.
var key_buf = std.ArrayList(u8).empty;
key_buf.appendSlice(self.alloc, ib.protocol_name) catch return;
for (arg_tys.items) |t| {
key_buf.append(self.alloc, 0) catch return;
key_buf.appendSlice(self.alloc, self.mangleTypeName(t)) catch return;
}
key_buf.append(self.alloc, 0) catch return;
key_buf.appendSlice(self.alloc, self.mangleTypeName(src_ty)) catch return;
const key = key_buf.items;
// Collect method fn_decl pointers.
var methods = std.ArrayList(*const ast.FnDecl).empty;
for (ib.methods) |method_node| {
if (method_node.data == .fn_decl) {
methods.append(self.alloc, &method_node.data.fn_decl) catch {};
}
}
const defining_module: []const u8 = self.current_source_file orelse "";
const entry: ParamImplEntry = .{
.methods = self.alloc.dupe(*const ast.FnDecl, methods.items) catch return,
.source_ty = src_ty,
.target_args = self.alloc.dupe(TypeId, arg_tys.items) catch return,
.defining_module = defining_module,
.span = decl.span,
};
const gop = self.param_impl_map.getOrPut(key) catch return;
if (!gop.found_existing) {
gop.value_ptr.* = std.ArrayList(ParamImplEntry).empty;
} else {
// Same-file duplicate is an immediate error. Cross-file overlaps
// are deferred to the xx resolution site (Phase 5) so the impl
// surface can be richer than any one file's view.
for (gop.value_ptr.items) |existing| {
if (std.mem.eql(u8, existing.defining_module, defining_module)) {
if (self.diagnostics) |diags| {
diags.addFmt(.err, decl.span, "duplicate impl '{s}' for source '{s}' in {s}", .{
ib.protocol_name, self.mangleTypeName(src_ty), defining_module,
});
}
return;
}
}
}
gop.value_ptr.append(self.alloc, entry) catch return;
}
/// Synthesize a fn_decl from a protocol default method for a concrete type.
fn synthesizeDefaultMethod(self: *Lowering, method: ast.ProtocolMethodDecl, target_type: []const u8) *const ast.FnDecl {
// Build parameter list: self: *TargetType, then the protocol method params
@@ -7957,15 +8159,15 @@ pub const Lowering = struct {
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);
}
// unbox: load the concrete value from the returned pointer. Real pointer
// returns (declared `-> *T` for non-Self T) are NOT auto-loaded — the
// pointee may be a single byte and reading `sizeof(target)` past it
// segfaults. Self is encoded as `*void`, so test against that exact type.
if (mi.ret_type == void_ptr) {
if (self.target_type) |target| {
const target_info = self.module.types.get(target);
if (target_info != .pointer) {
return self.builder.load(raw_result, target);
}
}
}
@@ -8334,7 +8536,13 @@ pub const Lowering = struct {
/// - int → int: widen/narrow
/// - int ↔ float: int_to_float/float_to_int
fn lowerXX(self: *Lowering, operand: Ref, operand_node: *const Node) Ref {
const src_ty = self.inferExprType(operand_node);
// Use the operand's *actual* lowered Ref type rather than reaching
// back through inferExprType — the latter doesn't cover every
// expression shape (notably lambdas), and a wrong src_ty here can
// route the cast through coerceToType (e.g. a bogus s64→ptr bitcast)
// and silently skip the user-space Into fallback.
const src_ty = self.builder.getRefType(operand);
const target_explicit = self.target_type != null;
const dst_ty = self.target_type orelse .s64;
// Any → concrete type: unbox
@@ -8377,7 +8585,140 @@ pub const Lowering = struct {
return self.buildProtocolErasure(operand, operand_node, src_ty, dst_ty);
}
return self.coerceToType(operand, src_ty, dst_ty);
const result = self.coerceToType(operand, src_ty, dst_ty);
// User-space fallback via `impl Into(Target) for Source`. Only fires
// when the target was explicitly named (not the .s64 default), src and
// dst differ, and the built-in ladder made no progress. Built-ins
// always win.
if (target_explicit and src_ty != dst_ty and result == operand) {
if (self.tryUserConversion(operand, operand_node, src_ty, dst_ty)) |converted| {
return converted;
}
// Pointer-target fallback: `xx <expr>` whose surrounding context
// expects `*T` (a fn arg slot, a var typed as a pointer-to-aggregate)
// can be satisfied by `impl Into(T) for src` plus an implicit
// alloca+store on the result. Lets users write
// `fn(xx () => { ... })` instead of materialising a named Block local
// just to take its address.
if (!dst_ty.isBuiltin()) {
const dst_info = self.module.types.get(dst_ty);
if (dst_info == .pointer) {
const pointee = dst_info.pointer.pointee;
if (pointee != src_ty) {
if (self.tryUserConversion(operand, operand_node, src_ty, pointee)) |converted| {
const slot = self.builder.alloca(pointee);
self.builder.store(slot, converted);
return slot;
}
}
}
}
}
return result;
}
/// Look up `Into(dst_ty)` impl for `src_ty` and, if found, monomorphise
/// the impl's `convert` method and emit a direct call. Returns null when
/// no impl matches (caller falls back to the built-in result, which is
/// the unchanged operand — Phase 3 emits no diagnostic for v0).
fn tryUserConversion(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) ?Ref {
// Reentrancy guard — pack (src, dst) into a u64.
const guard_key: u64 = (@as(u64, src_ty.index()) << 32) | @as(u64, dst_ty.index());
if (self.xx_reentrancy.contains(guard_key)) {
if (self.diagnostics) |diags| {
diags.addFmt(.err, operand_node.span, "recursive xx conversion from '{s}' to '{s}'", .{
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
});
}
return operand;
}
// Build lookup key: "Into\x00<dst_mangled>\x00<src_mangled>".
// Hardcoded to the "Into" protocol for v1. Generalising to other
// parameterised protocols would walk protocol_decl_map looking for
// protocols that take a single type-param and have a `convert` method.
const proto_name = "Into";
const pd = self.protocol_ast_map.get(proto_name) orelse return null;
if (pd.type_params.len != 1) return null;
var key_buf = std.ArrayList(u8).empty;
key_buf.appendSlice(self.alloc, proto_name) catch return null;
key_buf.append(self.alloc, 0) catch return null;
key_buf.appendSlice(self.alloc, self.mangleTypeName(dst_ty)) catch return null;
key_buf.append(self.alloc, 0) catch return null;
key_buf.appendSlice(self.alloc, self.mangleTypeName(src_ty)) catch return null;
const key = key_buf.items;
const entries = self.param_impl_map.get(key) orelse return null;
if (entries.items.len == 0) return null;
// Filter by import visibility: only impls in modules that the current
// file transitively imports (or the current file itself) are reachable.
// Falls open when import_graph isn't wired (e.g. comptime callers).
var visible_impls = std.ArrayList(ParamImplEntry).empty;
defer visible_impls.deinit(self.alloc);
self.findVisibleImpls(entries.items, &visible_impls);
if (visible_impls.items.len == 0) {
if (self.diagnostics) |diags| {
const saved = diags.current_source_file;
diags.current_source_file = operand_node.source_file orelse self.current_source_file;
defer diags.current_source_file = saved;
diags.addFmt(.err, operand_node.span, "no visible xx conversion from '{s}' to '{s}' — impl exists in another module but is not imported", .{
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
});
}
return operand;
}
if (visible_impls.items.len > 1) {
if (self.diagnostics) |diags| {
const saved = diags.current_source_file;
diags.current_source_file = operand_node.source_file orelse self.current_source_file;
defer diags.current_source_file = saved;
diags.addFmt(.err, operand_node.span, "duplicate xx conversion from '{s}' to '{s}': impls in {s} and {s}", .{
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
visible_impls.items[0].defining_module, visible_impls.items[1].defining_module,
});
}
return operand;
}
const entry = visible_impls.items[0];
// Find the `convert` method on this impl.
var convert_fd: ?*const ast.FnDecl = null;
for (entry.methods) |m| {
if (std.mem.eql(u8, m.name, "convert")) {
convert_fd = m;
break;
}
}
const fd = convert_fd orelse return null;
// Bind Target → dst_ty.
var bindings = std.StringHashMap(TypeId).init(self.alloc);
defer bindings.deinit();
bindings.put(pd.type_params[0].name, dst_ty) catch return null;
// Mangled name: "<src>.convert__<dst>".
const mangled = std.fmt.allocPrint(self.alloc, "{s}.convert__{s}", .{
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
}) catch return null;
self.xx_reentrancy.put(guard_key, {}) catch {};
defer _ = self.xx_reentrancy.remove(guard_key);
if (!self.lowered_functions.contains(mangled)) {
self.monomorphizeFunction(fd, mangled, &bindings);
}
const fid = self.resolveFuncByName(mangled) orelse return null;
const func = &self.module.functions.items[@intFromEnum(fid)];
const ret_ty = func.ret;
const params = func.params;
var args = [_]Ref{operand};
self.coerceCallArgs(args[0..], params);
return self.builder.call(fid, args[0..], ret_ty);
}
/// Build a protocol value from a concrete value via xx conversion.