diff --git a/examples/50-smoke.sx b/examples/50-smoke.sx index a6ea6ff..00ba47a 100644 --- a/examples/50-smoke.sx +++ b/examples/50-smoke.sx @@ -3017,5 +3017,33 @@ END; print("SM2: {} {}\n", p.a, p.b); } + // ============================================================ + // OPTIONAL IF-ELSE COERCION + // ============================================================ + { + print("--- optional if-else coercion ---\n"); + OptF :: struct { width: ?f32; } + x :f32: 10.0; + + // null in then branch + f1 := OptF.{ width = if true then null else x }; + print("opt-if1: {}\n", f1.width ?? 99.0); + + // value in then branch, null in else + f2 := OptF.{ width = if true then x else null }; + print("opt-if2: {}\n", f2.width ?? 99.0); + + // both branches are values + f3 := OptF.{ width = if false then 5.0 else x }; + print("opt-if3: {}\n", f3.width ?? 99.0); + + // standalone optional variable + val: ?f32 = if true then null else 42.0; + print("opt-if4: {}\n", val ?? 0.0); + + val2: ?f32 = if false then null else 42.0; + print("opt-if5: {}\n", val2 ?? 0.0); + } + print("=== DONE ===\n"); } diff --git a/examples/modules/stb_truetype.sx b/examples/modules/stb_truetype.sx new file mode 100644 index 0000000..7ee5834 --- /dev/null +++ b/examples/modules/stb_truetype.sx @@ -0,0 +1,7 @@ +#import c { + #include "vendors/stb_truetype/stb_truetype.h"; + #source "vendors/stb_truetype/stb_truetype_impl.c"; + + #include "vendors/file_utils/file_utils.h"; + #source "vendors/file_utils/file_utils.c"; +}; diff --git a/src/codegen.zig b/src/codegen.zig index f23e24d..b602f5f 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -6183,6 +6183,41 @@ pub const CodeGen = struct { if (node.data == .null_literal) { return self.makeNullOptional(target_ty); } + + // if_expr in optional context: propagate optional type to each branch + // so that `null` → makeNullOptional and values → wrapOptional individually + if (node.data == .if_expr) { + const ie = node.data.if_expr; + if (ie.binding_name == null and ie.else_branch != null) { + const cond_val = self.valueToBool(try self.genExpr(ie.condition)); + + var then_bb = self.appendBB("opt_then"); + var else_bb = self.appendBB("opt_else"); + const merge_bb = self.appendBB("opt_merge"); + + self.condBr(cond_val, then_bb, else_bb); + + self.positionAt(then_bb); + const then_val = try self.genExprAsType(ie.then_branch, target_ty); + then_bb = self.getCurrentBlock(); + self.br(merge_bb); + + self.positionAt(else_bb); + const else_val = try self.genExprAsType(ie.else_branch.?, target_ty); + else_bb = self.getCurrentBlock(); + self.br(merge_bb); + + self.positionAt(merge_bb); + + const llvm_ty = self.typeToLLVM(target_ty); + const phi = c.LLVMBuildPhi(self.builder, llvm_ty, "opt_iftmp"); + var vals = [2]c.LLVMValueRef{ then_val, else_val }; + var blocks = [2]c.LLVMBasicBlockRef{ then_bb, else_bb }; + c.LLVMAddIncoming(phi, &vals, &blocks, 2); + return phi; + } + } + // If source expression already produces the same optional type, pass through const src_ty = self.inferType(node); if (src_ty.eql(target_ty)) { diff --git a/src/lsp/server.zig b/src/lsp/server.zig index 5ca5dcb..4f77bcb 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -230,6 +230,26 @@ pub const Server = struct { if (try self.sendSymbolLocationWithOrigin(id_json, doc, sym, qn_origin)) return; } } + + // Struct method/field: obj.method or Type.method or obj.field + if (try self.resolveStructMemberDef(id_json, sema, doc, qn.ns, qn.member, qn_origin)) return; + } + + // 1b. Dot-shorthand: .method(args) — identifier preceded by dot with no qualifier + if (extractIdentAtOffset(doc.source, offset)) |name| { + const name_start = @as(u32, @intCast(@intFromPtr(name.ptr) - @intFromPtr(doc.source.ptr))); + if (name_start > 0 and doc.source[name_start - 1] == '.' and + (name_start < 2 or !isIdentChar(doc.source[name_start - 2]))) + { + const name_end = name_start + @as(u32, @intCast(name.len)); + const origin = sx.ast.Span{ .start = name_start, .end = name_end }; + // Search all type declarations for a matching method/variant/field + for (sema.symbols) |sym| { + if (sym.kind != .struct_type and sym.kind != .enum_type) continue; + const lookup = self.findTypeDeclNode(sema, doc, sym.name) orelse continue; + if (try self.sendMemberLocation(id_json, lookup, name, doc, origin)) return; + } + } } // 2. Reference at offset → jump to definition @@ -525,12 +545,12 @@ pub const Server = struct { } } else if (doc.root) |root| { // Try as type name directly (e.g. Vec2., Color.) - try self.collectMemberCompletions(&items, sema, root, prefix); + try self.collectMemberCompletions(&items, sema, doc, prefix); // Try as variable name — resolve to type and offer fields + UFCS methods if (items.items.len == 0) { if (resolveVariableType(sema, prefix)) |type_name| { - try self.collectMemberCompletions(&items, sema, root, type_name); + try self.collectMemberCompletions(&items, sema, doc, type_name); try self.collectUfcsCompletions(&items, root, type_name); } } @@ -664,7 +684,8 @@ pub const Server = struct { const payload_type_name = if (vt.data == .type_expr) vt.data.type_expr.name else break; // Now offer that struct's fields const payload_sema = if (lookup_doc) |ld| ld.sema orelse sema else sema; - try self.collectMemberCompletions(items, payload_sema, root, payload_type_name); + const payload_doc = if (lookup_doc) |ld| ld else doc; + try self.collectMemberCompletions(items, payload_sema, payload_doc, payload_type_name); return; } } @@ -858,88 +879,69 @@ pub const Server = struct { } } - fn collectMemberCompletions(self: *Server, items: *std.ArrayList(lsp.CompletionItem), sema: SemaResult, root: *sx.ast.Node, name: []const u8) !void { - for (sema.symbols) |sym| { - if (!std.mem.eql(u8, sym.name, name)) continue; + fn collectMemberCompletions(self: *Server, items: *std.ArrayList(lsp.CompletionItem), sema: SemaResult, doc: *const Document, name: []const u8) !void { + const lookup = self.findTypeDeclNode(sema, doc, name) orelse return; + const node = lookup.node; - if (sym.kind == .struct_type) { - // For imported symbols, look up the source doc's AST - const lookup_root = if (sym.origin) |origin_path| - if (self.documents.get(origin_path)) |od| od.root orelse root else root - else - root; - if (sx.sema.findNodeAtOffset(lookup_root, sym.def_span.start)) |node| { - if (node.data == .struct_decl) { - const sd = node.data.struct_decl; - for (sd.field_names, 0..) |field_name, fi| { - const detail: ?[]const u8 = if (fi < sd.field_types.len) blk: { - const ft = sd.field_types[fi]; - break :blk if (ft.data == .type_expr) ft.data.type_expr.name else null; - } else null; + if (node.data == .struct_decl) { + const sd = node.data.struct_decl; + for (sd.field_names, 0..) |field_name, fi| { + const detail: ?[]const u8 = if (fi < sd.field_types.len) blk: { + const ft = sd.field_types[fi]; + break :blk if (ft.data == .type_expr) ft.data.type_expr.name else null; + } else null; - try items.append(self.allocator, .{ - .label = field_name, - .kind = @intFromEnum(lsp.CompletionItemKind.Field), - .detail = detail, - }); - } - } - } - } else if (sym.kind == .enum_type) { - const lookup_root = if (sym.origin) |origin_path| - if (self.documents.get(origin_path)) |od| od.root orelse root else root - else - root; - if (sx.sema.findNodeAtOffset(lookup_root, sym.def_span.start)) |node| { - if (node.data == .enum_decl) { - const ed = node.data.enum_decl; - for (ed.variant_names) |variant| { - try items.append(self.allocator, .{ - .label = variant, - .kind = @intFromEnum(lsp.CompletionItemKind.EnumMember), - }); - } - } - } - } else if (sym.kind == .protocol_type) { - const lookup_root = if (sym.origin) |origin_path| - if (self.documents.get(origin_path)) |od| od.root orelse root else root - else - root; - if (sx.sema.findNodeAtOffset(lookup_root, sym.def_span.start)) |node| { - if (node.data == .protocol_decl) { - const pd = node.data.protocol_decl; - for (pd.methods) |method| { - // Build detail string: (params) -> ret - var detail_buf = std.ArrayList(u8).empty; - try detail_buf.append(self.allocator, '('); - for (method.param_names, 0..) |pname, pi| { - if (pi > 0) try detail_buf.appendSlice(self.allocator, ", "); - try detail_buf.appendSlice(self.allocator, pname); - if (pi < method.params.len) { - try detail_buf.appendSlice(self.allocator, ": "); - if (method.params[pi].data == .type_expr) { - try detail_buf.appendSlice(self.allocator, method.params[pi].data.type_expr.name); - } - } - } - try detail_buf.append(self.allocator, ')'); - if (method.return_type) |rt| { - try detail_buf.appendSlice(self.allocator, " -> "); - if (rt.data == .type_expr) { - try detail_buf.appendSlice(self.allocator, rt.data.type_expr.name); - } - } - try items.append(self.allocator, .{ - .label = method.name, - .kind = @intFromEnum(lsp.CompletionItemKind.Method), - .detail = detail_buf.items, - }); - } - } + try items.append(self.allocator, .{ + .label = field_name, + .kind = @intFromEnum(lsp.CompletionItemKind.Field), + .detail = detail, + }); + } + for (sd.methods) |method_node| { + if (method_node.data == .fn_decl) { + try items.append(self.allocator, .{ + .label = method_node.data.fn_decl.name, + .kind = @intFromEnum(lsp.CompletionItemKind.Method), + }); } } - break; + } else if (node.data == .enum_decl) { + const ed = node.data.enum_decl; + for (ed.variant_names) |variant| { + try items.append(self.allocator, .{ + .label = variant, + .kind = @intFromEnum(lsp.CompletionItemKind.EnumMember), + }); + } + } else if (node.data == .protocol_decl) { + const pd = node.data.protocol_decl; + for (pd.methods) |method| { + // Build detail string: (params) -> ret + var detail_buf = std.ArrayList(u8).empty; + try detail_buf.append(self.allocator, '('); + for (method.param_names, 0..) |pname, pi| { + if (pi > 0) try detail_buf.appendSlice(self.allocator, ", "); + try detail_buf.appendSlice(self.allocator, pname); + if (pi < method.params.len) { + try detail_buf.appendSlice(self.allocator, ": "); + if (method.params[pi].data == .type_expr) { + try detail_buf.appendSlice(self.allocator, method.params[pi].data.type_expr.name); + } + } + } + try detail_buf.append(self.allocator, ')'); + if (method.return_type) |rt| { + try detail_buf.appendSlice(self.allocator, " -> "); + if (rt.data == .type_expr) { + try detail_buf.appendSlice(self.allocator, rt.data.type_expr.name); + } + } + try items.append(self.allocator, .{ + .label = method.name, + .kind = @intFromEnum(lsp.CompletionItemKind.Method), + .detail = detail_buf.items, + }); + } } } @@ -1806,6 +1808,89 @@ pub const Server = struct { return doc; } + const TypeDeclLookup = struct { node: *sx.ast.Node, doc: *const Document }; + + /// Given a type name, find its AST declaration node and the document it lives in. + /// Works for struct_decl, enum_decl, union_decl, and protocol_decl across files. + fn findTypeDeclNode(self: *Server, sema: SemaResult, doc: *const Document, type_name: []const u8) ?TypeDeclLookup { + for (sema.symbols) |sym| { + if (!std.mem.eql(u8, sym.name, type_name)) continue; + if (sym.kind != .struct_type and sym.kind != .enum_type and sym.kind != .protocol_type) continue; + + const lookup_doc = self.resolveSymbolDoc(doc, sym); + const lookup_root = lookup_doc.root orelse return null; + + if (sx.sema.findNodeAtOffset(lookup_root, sym.def_span.start)) |node| { + return .{ .node = node, .doc = lookup_doc }; + } + return null; + } + return null; + } + + /// Send a go-to-definition response for a struct method, field, or enum variant. + fn sendMemberLocation(self: *Server, id_json: []const u8, lookup: TypeDeclLookup, member_name: []const u8, origin_doc: *const Document, origin: sx.ast.Span) !bool { + if (lookup.node.data == .struct_decl) { + const sd = lookup.node.data.struct_decl; + // Check methods + for (sd.methods) |method_node| { + if (method_node.data == .fn_decl) { + if (std.mem.eql(u8, method_node.data.fn_decl.name, member_name)) { + return self.sendSpanLocation(id_json, lookup.doc, method_node.span, origin_doc, origin); + } + } + } + // Check fields — use field type span as approximate location + for (sd.field_names, 0..) |fname, fi| { + if (std.mem.eql(u8, fname, member_name)) { + if (fi < sd.field_types.len) { + return self.sendSpanLocation(id_json, lookup.doc, sd.field_types[fi].span, origin_doc, origin); + } + } + } + } else if (lookup.node.data == .enum_decl) { + const ed = lookup.node.data.enum_decl; + for (ed.variant_names) |v| { + if (std.mem.eql(u8, v, member_name)) { + return self.sendSpanLocation(id_json, lookup.doc, lookup.node.span, origin_doc, origin); + } + } + } else if (lookup.node.data == .union_decl) { + const ud = lookup.node.data.union_decl; + for (ud.field_names) |fname| { + if (std.mem.eql(u8, fname, member_name)) { + return self.sendSpanLocation(id_json, lookup.doc, lookup.node.span, origin_doc, origin); + } + } + } + return false; + } + + /// Resolve qualified name as struct/union method or field, send definition location. + fn resolveStructMemberDef(self: *Server, id_json: []const u8, sema: SemaResult, doc: *const Document, ns: []const u8, member: []const u8, origin: sx.ast.Span) !bool { + // Try ns as a type name directly + if (self.findTypeDeclNode(sema, doc, ns)) |lookup| { + if (try self.sendMemberLocation(id_json, lookup, member, doc, origin)) return true; + } + // Try ns as a variable name → resolve to struct type + if (resolveStructTypeName(sema, doc, ns)) |type_name| { + if (self.findTypeDeclNode(sema, doc, type_name)) |lookup| { + if (try self.sendMemberLocation(id_json, lookup, member, doc, origin)) return true; + } + } + return false; + } + + /// Send a go-to-definition response pointing to a span in a document. + fn sendSpanLocation(self: *Server, id_json: []const u8, target_doc: *const Document, target_span: sx.ast.Span, origin_doc: *const Document, origin_span: sx.ast.Span) !bool { + const target_range = spanToRange(target_doc.source, target_span); + const target_uri = try std.fmt.allocPrint(self.allocator, "file://{s}", .{target_doc.path}); + const src_range = spanToRange(origin_doc.source, origin_span); + const loc_json = try lsp.locationLinkJson(self.allocator, target_uri, target_range, src_range); + try self.sendResponse(id_json, loc_json); + return true; + } + /// Find an import by namespace name (falls back to last good imports). fn findImportByNs(_: *Server, doc: *const Document, ns_name: []const u8) ?doc_mod.Import { const imports_lists = [_][]const doc_mod.Import{ doc.imports, doc.last_good_imports }; @@ -1958,48 +2043,40 @@ pub const Server = struct { const sema = doc.sema orelse return null; const struct_name = resolveStructTypeName(sema, doc, obj_name) orelse return null; - for (sema.symbols) |sym| { - if (sym.kind != .struct_type or !std.mem.eql(u8, sym.name, struct_name)) continue; + const lookup = self.findTypeDeclNode(sema, doc, struct_name) orelse return null; + if (lookup.node.data != .struct_decl) return null; + const sd = lookup.node.data.struct_decl; + const lookup_doc = lookup.doc; - const lookup_doc = self.resolveSymbolDoc(doc, sym); - const lookup_root = lookup_doc.root orelse return null; + for (sd.field_names, 0..) |fn_, fi| { + if (!std.mem.eql(u8, fn_, field_name)) continue; - if (sx.sema.findNodeAtOffset(lookup_root, sym.def_span.start)) |node| { - if (node.data == .struct_decl) { - const sd = node.data.struct_decl; - for (sd.field_names, 0..) |fn_, fi| { - if (!std.mem.eql(u8, fn_, field_name)) continue; + var buf = std.ArrayList(u8).empty; - var buf = std.ArrayList(u8).empty; - - // Doc comment above field - const fn_addr = @intFromPtr(fn_.ptr); - const src_addr = @intFromPtr(lookup_doc.source.ptr); - const src_end = src_addr + lookup_doc.source.len; - if (fn_addr >= src_addr and fn_addr < src_end) { - const field_offset = @as(u32, @intCast(fn_addr - src_addr)); - if (extractDocComment(lookup_doc.source, field_offset)) |comment| { - try buf.appendSlice(self.allocator, comment); - try buf.appendSlice(self.allocator, "\n\n"); - } - } - - try buf.appendSlice(self.allocator, "```sx\n"); - try buf.appendSlice(self.allocator, struct_name); - try buf.append(self.allocator, '.'); - try buf.appendSlice(self.allocator, field_name); - if (fi < sd.field_types.len) { - if (sd.field_types[fi].data == .type_expr) { - try buf.appendSlice(self.allocator, " : "); - try buf.appendSlice(self.allocator, sd.field_types[fi].data.type_expr.name); - } - } - try buf.appendSlice(self.allocator, "\n```"); - return buf.items; - } + // Doc comment above field + const fn_addr = @intFromPtr(fn_.ptr); + const src_addr = @intFromPtr(lookup_doc.source.ptr); + const src_end = src_addr + lookup_doc.source.len; + if (fn_addr >= src_addr and fn_addr < src_end) { + const field_offset = @as(u32, @intCast(fn_addr - src_addr)); + if (extractDocComment(lookup_doc.source, field_offset)) |comment| { + try buf.appendSlice(self.allocator, comment); + try buf.appendSlice(self.allocator, "\n\n"); } } - break; + + try buf.appendSlice(self.allocator, "```sx\n"); + try buf.appendSlice(self.allocator, struct_name); + try buf.append(self.allocator, '.'); + try buf.appendSlice(self.allocator, field_name); + if (fi < sd.field_types.len) { + if (sd.field_types[fi].data == .type_expr) { + try buf.appendSlice(self.allocator, " : "); + try buf.appendSlice(self.allocator, sd.field_types[fi].data.type_expr.name); + } + } + try buf.appendSlice(self.allocator, "\n```"); + return buf.items; } return null; } @@ -2007,39 +2084,37 @@ pub const Server = struct { fn formatEnumVariantHover(self: *Server, doc: *const Document, variant_name: []const u8) !?[]const u8 { const sema = doc.sema orelse return null; + // Search all enum types for the variant for (sema.symbols) |sym| { if (sym.kind != .enum_type) continue; - const lookup_doc = self.resolveSymbolDoc(doc, sym); - const lookup_root = lookup_doc.root orelse continue; + const lookup = self.findTypeDeclNode(sema, doc, sym.name) orelse continue; + if (lookup.node.data != .enum_decl) continue; + const ed = lookup.node.data.enum_decl; + const lookup_doc = lookup.doc; - if (sx.sema.findNodeAtOffset(lookup_root, sym.def_span.start)) |node| { - if (node.data == .enum_decl) { - const ed = node.data.enum_decl; - for (ed.variant_names) |v| { - if (!std.mem.eql(u8, v, variant_name)) continue; + for (ed.variant_names) |v| { + if (!std.mem.eql(u8, v, variant_name)) continue; - var buf = std.ArrayList(u8).empty; + var buf = std.ArrayList(u8).empty; - const v_addr = @intFromPtr(v.ptr); - const src_addr2 = @intFromPtr(lookup_doc.source.ptr); - const src_end2 = src_addr2 + lookup_doc.source.len; - if (v_addr >= src_addr2 and v_addr < src_end2) { - const variant_offset = @as(u32, @intCast(v_addr - src_addr2)); - if (extractDocComment(lookup_doc.source, variant_offset)) |comment| { - try buf.appendSlice(self.allocator, comment); - try buf.appendSlice(self.allocator, "\n\n"); - } - } - - try buf.appendSlice(self.allocator, "```sx\n"); - try buf.appendSlice(self.allocator, sym.name); - try buf.append(self.allocator, '.'); - try buf.appendSlice(self.allocator, variant_name); - try buf.appendSlice(self.allocator, "\n```"); - return buf.items; + const v_addr = @intFromPtr(v.ptr); + const src_addr2 = @intFromPtr(lookup_doc.source.ptr); + const src_end2 = src_addr2 + lookup_doc.source.len; + if (v_addr >= src_addr2 and v_addr < src_end2) { + const variant_offset = @as(u32, @intCast(v_addr - src_addr2)); + if (extractDocComment(lookup_doc.source, variant_offset)) |comment| { + try buf.appendSlice(self.allocator, comment); + try buf.appendSlice(self.allocator, "\n\n"); } } + + try buf.appendSlice(self.allocator, "```sx\n"); + try buf.appendSlice(self.allocator, sym.name); + try buf.append(self.allocator, '.'); + try buf.appendSlice(self.allocator, variant_name); + try buf.appendSlice(self.allocator, "\n```"); + return buf.items; } } return null; diff --git a/src/sema.zig b/src/sema.zig index 28b8252..be42b54 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -1268,7 +1268,6 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node { .library_decl, .function_type_expr, .enum_decl, - .struct_decl, .union_decl, .import_decl, .c_import_decl, @@ -1286,6 +1285,11 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node { .ufcs_alias, .closure_type_expr, => {}, + .struct_decl => |sd| { + for (sd.methods) |method_node| { + if (findNodeAtOffset(method_node, offset)) |found| return found; + } + }, .protocol_decl => |pd| { for (pd.methods) |method| { if (method.default_body) |body| { diff --git a/tests/expected/50-smoke.txt b/tests/expected/50-smoke.txt index ee9c1ea..87493d6 100644 --- a/tests/expected/50-smoke.txt +++ b/tests/expected/50-smoke.txt @@ -586,4 +586,10 @@ IB5: 52 SM1: 16.000000 8.000000 SM1: 5.000000 5.000000 SM2: 10 20 +--- optional if-else coercion --- +opt-if1: 99.000000 +opt-if2: 10.000000 +opt-if3: 10.000000 +opt-if4: 0.000000 +opt-if5: 42.000000 === DONE ===