iOS lock step keyboard + metal
This commit is contained in:
@@ -241,6 +241,9 @@ pub const VarDecl = struct {
|
||||
name: []const u8,
|
||||
type_annotation: ?*Node,
|
||||
value: ?*Node,
|
||||
is_foreign: bool = false,
|
||||
foreign_lib: ?[]const u8 = null,
|
||||
foreign_name: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const Assignment = struct {
|
||||
@@ -506,6 +509,7 @@ pub const ProtocolDecl = struct {
|
||||
name: []const u8,
|
||||
methods: []const ProtocolMethodDecl,
|
||||
is_inline: bool = false, // #inline — embedded fn ptrs instead of vtable pointer
|
||||
type_params: []const StructTypeParam = &.{}, // for `protocol(Target: Type) { ... }`
|
||||
};
|
||||
|
||||
pub const ImplBlock = struct {
|
||||
@@ -513,4 +517,6 @@ pub const ImplBlock = struct {
|
||||
target_type: []const u8,
|
||||
target_type_params: []const StructTypeParam = &.{}, // for `impl P for List($T)`
|
||||
methods: []const *Node, // fn_decl nodes
|
||||
protocol_type_args: []const *Node = &.{}, // for `impl Into(Block) for Source` — type args on the protocol side
|
||||
target_type_expr: ?*Node = null, // populated for parameterised-protocol impls; carries non-identifier source spellings (e.g. `Closure() -> void`)
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ pub const Compilation = struct {
|
||||
resolved_root: ?*Node = null,
|
||||
import_sources: std.StringHashMap([:0]const u8),
|
||||
module_scopes: std.StringHashMap(std.StringHashMap(void)),
|
||||
import_graph: std.StringHashMap(std.StringHashMap(void)),
|
||||
sema_result: ?sema.SemaResult = null,
|
||||
ir_emitter: ?ir.LLVMEmitter = null,
|
||||
|
||||
@@ -37,6 +38,7 @@ pub const Compilation = struct {
|
||||
.diagnostics = errors.DiagnosticList.init(allocator, source, file_path),
|
||||
.import_sources = std.StringHashMap([:0]const u8).init(allocator),
|
||||
.module_scopes = std.StringHashMap(std.StringHashMap(void)).init(allocator),
|
||||
.import_graph = std.StringHashMap(std.StringHashMap(void)).init(allocator),
|
||||
.target_config = target_config,
|
||||
.stdlib_paths = stdlib_paths,
|
||||
};
|
||||
@@ -69,6 +71,7 @@ pub const Compilation = struct {
|
||||
&self.import_sources,
|
||||
&self.diagnostics,
|
||||
self.stdlib_paths,
|
||||
&self.import_graph,
|
||||
) catch return error.CompileError;
|
||||
|
||||
// Preserve per-module visibility scopes for C import access checking
|
||||
@@ -162,6 +165,7 @@ pub const Compilation = struct {
|
||||
lowering.target_config = self.target_config;
|
||||
lowering.diagnostics = &self.diagnostics;
|
||||
lowering.module_scopes = &self.module_scopes;
|
||||
lowering.import_graph = &self.import_graph;
|
||||
lowering.lowerRoot(root);
|
||||
if (self.diagnostics.hasErrors()) return error.CompileError;
|
||||
return module;
|
||||
|
||||
@@ -175,7 +175,16 @@ pub fn resolveImports(
|
||||
source_map: ?*std.StringHashMap([:0]const u8),
|
||||
diagnostics: ?*errors.DiagnosticList,
|
||||
stdlib_paths: []const []const u8,
|
||||
import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
|
||||
) !ResolvedModule {
|
||||
// Record this file's edge set so `param_impl_map` lookups can filter
|
||||
// candidates by what's been imported from where. Populated as each
|
||||
// import resolves below; transitive closure computed on demand.
|
||||
if (import_graph) |g| {
|
||||
if (!g.contains(file_path)) {
|
||||
try g.put(file_path, std.StringHashMap(void).init(allocator));
|
||||
}
|
||||
}
|
||||
var mod = ResolvedModule{
|
||||
.path = file_path,
|
||||
.decls = &.{},
|
||||
@@ -246,6 +255,15 @@ pub fn resolveImports(
|
||||
|
||||
const resolved_path = try resolveImportPath(allocator, io, base_dir, imp.path, null, stdlib_paths);
|
||||
|
||||
// Record direct-import edge file_path → resolved_path. Self-imports
|
||||
// and chain duplicates are still recorded so the graph reflects what
|
||||
// the user wrote (filter happens at lookup).
|
||||
if (import_graph) |g| {
|
||||
if (g.getPtr(file_path)) |set| {
|
||||
set.put(resolved_path, {}) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
// Circular import check — only along the current chain
|
||||
if (chain.contains(resolved_path)) continue;
|
||||
|
||||
@@ -272,7 +290,7 @@ pub fn resolveImports(
|
||||
// Push onto chain before recursing, pop after
|
||||
try chain.put(resolved_path, {});
|
||||
const imp_dir = dirName(resolved_path);
|
||||
const result = try resolveImports(allocator, io, imp_root, imp_dir, resolved_path, chain, cache, source_map, diagnostics, stdlib_paths);
|
||||
const result = try resolveImports(allocator, io, imp_root, imp_dir, resolved_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph);
|
||||
_ = chain.remove(resolved_path);
|
||||
|
||||
// Cache
|
||||
@@ -280,7 +298,7 @@ pub fn resolveImports(
|
||||
break :blk result;
|
||||
} else |_| {
|
||||
// File read failed — try as directory import
|
||||
const result = resolveDirectoryImport(allocator, io, resolved_path, chain, cache, source_map, diagnostics, decl.span, stdlib_paths) catch {
|
||||
const result = resolveDirectoryImport(allocator, io, resolved_path, chain, cache, source_map, diagnostics, decl.span, stdlib_paths, import_graph) catch {
|
||||
if (diagnostics) |diags| {
|
||||
diags.addFmt(.err, decl.span, "cannot read import '{s}' (not a file or directory)", .{resolved_path});
|
||||
}
|
||||
@@ -313,6 +331,7 @@ fn resolveDirectoryImport(
|
||||
diagnostics: ?*errors.DiagnosticList,
|
||||
span: ast.Span,
|
||||
stdlib_paths: []const []const u8,
|
||||
import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
|
||||
) anyerror!ResolvedModule {
|
||||
// Open the directory with iteration capability
|
||||
const dir = std.Io.Dir.openDir(.cwd(), io, dir_path, .{ .iterate = true }) catch {
|
||||
@@ -378,7 +397,7 @@ fn resolveDirectoryImport(
|
||||
};
|
||||
|
||||
try chain.put(file_path, {});
|
||||
const result = try resolveImports(allocator, io, imp_root, dir_path, file_path, chain, cache, source_map, diagnostics, stdlib_paths);
|
||||
const result = try resolveImports(allocator, io, imp_root, dir_path, file_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph);
|
||||
_ = chain.remove(file_path);
|
||||
|
||||
try cache.put(file_path, result);
|
||||
|
||||
@@ -267,6 +267,15 @@ pub const LLVMEmitter = struct {
|
||||
defer self.alloc.free(name_z);
|
||||
|
||||
const llvm_global = c.LLVMAddGlobal(self.llvm_module, llvm_ty, name_z.ptr);
|
||||
|
||||
// Extern globals (`<name> : <type> #foreign;`) resolve at link time
|
||||
// to a libSystem / framework symbol — no initializer, default linkage.
|
||||
if (global.is_extern) {
|
||||
c.LLVMSetLinkage(llvm_global, c.LLVMExternalLinkage);
|
||||
self.global_map.put(@intCast(i), llvm_global) catch {};
|
||||
continue;
|
||||
}
|
||||
|
||||
c.LLVMSetLinkage(llvm_global, c.LLVMInternalLinkage);
|
||||
|
||||
// Evaluate comptime initializer if present
|
||||
|
||||
387
src/ir/lower.zig
387
src/ir/lower.zig
@@ -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.
|
||||
|
||||
143
src/parser.zig
143
src/parser.zig
@@ -362,7 +362,32 @@ pub const Parser = struct {
|
||||
return try self.createNode(start_pos, .{ .var_decl = .{ .name = name, .type_annotation = type_node, .value = null } });
|
||||
}
|
||||
|
||||
return self.fail("expected ':', '=' or ';' after type annotation");
|
||||
if (self.current.tag == .hash_foreign) {
|
||||
// name : type #foreign [lib] ["c_name"]; (extern global from libsystem etc.)
|
||||
self.advance();
|
||||
var lib_ref: ?[]const u8 = null;
|
||||
if (self.current.tag == .identifier) {
|
||||
lib_ref = self.tokenSlice(self.current);
|
||||
self.advance();
|
||||
}
|
||||
var c_name: ?[]const u8 = null;
|
||||
if (self.current.tag == .string_literal) {
|
||||
const raw = self.tokenSlice(self.current);
|
||||
c_name = raw[1 .. raw.len - 1];
|
||||
self.advance();
|
||||
}
|
||||
try self.expect(.semicolon);
|
||||
return try self.createNode(start_pos, .{ .var_decl = .{
|
||||
.name = name,
|
||||
.type_annotation = type_node,
|
||||
.value = null,
|
||||
.is_foreign = true,
|
||||
.foreign_lib = lib_ref,
|
||||
.foreign_name = c_name,
|
||||
} });
|
||||
}
|
||||
|
||||
return self.fail("expected ':', '=', ';' or '#foreign' after type annotation");
|
||||
}
|
||||
|
||||
fn parseTypeExpr(self: *Parser) anyerror!*Node {
|
||||
@@ -890,6 +915,29 @@ pub const Parser = struct {
|
||||
fn parseProtocolDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
|
||||
self.advance(); // skip 'protocol'
|
||||
|
||||
// Optional type params: protocol(Target: Type, U: Type) { ... }
|
||||
// Names are introduced without a `$` sigil (unlike struct's $T) because
|
||||
// the parens after `protocol` already mark this as a parameter list.
|
||||
var type_params = std.ArrayList(ast.StructTypeParam).empty;
|
||||
if (self.current.tag == .l_paren) {
|
||||
self.advance(); // skip '('
|
||||
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
||||
if (type_params.items.len > 0) {
|
||||
try self.expect(.comma);
|
||||
if (self.current.tag == .r_paren) break;
|
||||
}
|
||||
if (self.current.tag != .identifier) {
|
||||
return self.fail("expected type parameter name in protocol header");
|
||||
}
|
||||
const param_name = self.tokenSlice(self.current);
|
||||
self.advance();
|
||||
try self.expect(.colon);
|
||||
const constraint = try self.parseTypeExpr();
|
||||
try type_params.append(self.allocator, .{ .name = param_name, .constraint = constraint });
|
||||
}
|
||||
try self.expect(.r_paren);
|
||||
}
|
||||
|
||||
// Check for #inline
|
||||
var is_inline = false;
|
||||
if (self.current.tag == .hash_inline) {
|
||||
@@ -899,6 +947,14 @@ pub const Parser = struct {
|
||||
|
||||
try self.expect(.l_brace);
|
||||
|
||||
// Push type-param names into scope so method signatures can refer to them
|
||||
// bare (e.g. `convert :: () -> Target` resolves Target as a generic type expr).
|
||||
var tp_names = std.ArrayList([]const u8).empty;
|
||||
for (type_params.items) |tp| try tp_names.append(self.allocator, tp.name);
|
||||
const saved_struct_type_params = self.struct_type_params;
|
||||
self.struct_type_params = tp_names.items;
|
||||
defer self.struct_type_params = saved_struct_type_params;
|
||||
|
||||
var methods = std.ArrayList(ast.ProtocolMethodDecl).empty;
|
||||
|
||||
while (self.current.tag != .r_brace and self.current.tag != .eof) {
|
||||
@@ -962,6 +1018,7 @@ pub const Parser = struct {
|
||||
.name = name,
|
||||
.methods = try methods.toOwnedSlice(self.allocator),
|
||||
.is_inline = is_inline,
|
||||
.type_params = try type_params.toOwnedSlice(self.allocator),
|
||||
} });
|
||||
}
|
||||
|
||||
@@ -975,39 +1032,71 @@ pub const Parser = struct {
|
||||
const protocol_name = self.tokenSlice(self.current);
|
||||
self.advance();
|
||||
|
||||
// Optional protocol type args: impl Into(Block) for ...
|
||||
var protocol_type_args = std.ArrayList(*Node).empty;
|
||||
if (self.current.tag == .l_paren) {
|
||||
self.advance(); // skip '('
|
||||
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
||||
if (protocol_type_args.items.len > 0) {
|
||||
try self.expect(.comma);
|
||||
if (self.current.tag == .r_paren) break;
|
||||
}
|
||||
try protocol_type_args.append(self.allocator, try self.parseTypeExpr());
|
||||
}
|
||||
try self.expect(.r_paren);
|
||||
}
|
||||
|
||||
// 'for' — note: 'for' is a keyword (kw_for), not an identifier
|
||||
if (self.current.tag != .kw_for) {
|
||||
return self.fail("expected 'for' after protocol name in impl block");
|
||||
}
|
||||
self.advance();
|
||||
|
||||
// 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);
|
||||
self.advance();
|
||||
|
||||
// Optional type params: impl Protocol for List($T)
|
||||
// Source-type spelling. For parameterised protocols we accept any TypeExpr
|
||||
// (`Closure(...) -> R`, `*T`, etc.). For nullary protocols we keep the
|
||||
// legacy identifier-only path so existing `impl P for SomeStruct` keeps
|
||||
// working unchanged (the parser doesn't try to over-parse trailing tokens).
|
||||
var target_type: []const u8 = "";
|
||||
var target_type_expr: ?*Node = null;
|
||||
var target_type_params = std.ArrayList(ast.StructTypeParam).empty;
|
||||
if (self.current.tag == .l_paren) {
|
||||
self.advance(); // skip '('
|
||||
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
||||
if (target_type_params.items.len > 0) {
|
||||
try self.expect(.comma);
|
||||
if (self.current.tag == .r_paren) break;
|
||||
}
|
||||
try self.expect(.dollar);
|
||||
if (self.current.tag != .identifier) {
|
||||
return self.fail("expected type parameter name after '$'");
|
||||
}
|
||||
const param_name = self.tokenSlice(self.current);
|
||||
self.advance();
|
||||
// Optional constraint — for now just use Type
|
||||
const constraint = try self.createNode(self.current.loc.start, .{ .type_expr = .{ .name = "Type" } });
|
||||
try target_type_params.append(self.allocator, .{ .name = param_name, .constraint = constraint });
|
||||
|
||||
if (protocol_type_args.items.len > 0) {
|
||||
// Parameterised protocol — source is a general TypeExpr.
|
||||
target_type_expr = try self.parseTypeExpr();
|
||||
// Synthesize a string view of the source for back-compat consumers
|
||||
// (LSP hover, etc.). The semantic key for the impl map uses
|
||||
// structural mangling, not this string.
|
||||
if (target_type_expr.?.data == .type_expr) {
|
||||
target_type = target_type_expr.?.data.type_expr.name;
|
||||
}
|
||||
} else {
|
||||
// Legacy nullary-protocol path: single identifier source.
|
||||
if (self.current.tag != .identifier and !self.current.tag.isTypeKeyword()) {
|
||||
return self.fail("expected type name after 'for'");
|
||||
}
|
||||
target_type = self.tokenSlice(self.current);
|
||||
self.advance();
|
||||
|
||||
// Optional type params: impl Protocol for List($T)
|
||||
if (self.current.tag == .l_paren) {
|
||||
self.advance(); // skip '('
|
||||
while (self.current.tag != .r_paren and self.current.tag != .eof) {
|
||||
if (target_type_params.items.len > 0) {
|
||||
try self.expect(.comma);
|
||||
if (self.current.tag == .r_paren) break;
|
||||
}
|
||||
try self.expect(.dollar);
|
||||
if (self.current.tag != .identifier) {
|
||||
return self.fail("expected type parameter name after '$'");
|
||||
}
|
||||
const param_name = self.tokenSlice(self.current);
|
||||
self.advance();
|
||||
// Optional constraint — for now just use Type
|
||||
const constraint = try self.createNode(self.current.loc.start, .{ .type_expr = .{ .name = "Type" } });
|
||||
try target_type_params.append(self.allocator, .{ .name = param_name, .constraint = constraint });
|
||||
}
|
||||
try self.expect(.r_paren);
|
||||
}
|
||||
try self.expect(.r_paren);
|
||||
}
|
||||
|
||||
try self.expect(.l_brace);
|
||||
@@ -1045,6 +1134,8 @@ pub const Parser = struct {
|
||||
.target_type = target_type,
|
||||
.target_type_params = try target_type_params.toOwnedSlice(self.allocator),
|
||||
.methods = try methods.toOwnedSlice(self.allocator),
|
||||
.protocol_type_args = try protocol_type_args.toOwnedSlice(self.allocator),
|
||||
.target_type_expr = target_type_expr,
|
||||
} });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user