From 566121c45a83189761699563962d7f6b7f14dfee Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 24 Feb 2026 17:37:52 +0200 Subject: [PATCH] more forward declarations --- examples/11-vector-math.sx | 24 +- examples/15-while.sx | 2 +- examples/22-anytype.sx | 2 +- examples/35-closures.sx | 1 + examples/50-smoke.sx | 5 +- examples/issue-0002.sx | 25 ++ examples/modules/testpkg/cwd_test.sx | 5 +- examples/test_union_array.sx | 14 + specs.md | 8 +- src/codegen.zig | 176 ++++++--- src/lsp/server.zig | 83 +++- src/sema.zig | 44 ++- tests/expected/50-smoke.txt | 566 ++++++++++++++++++++++++++- 13 files changed, 867 insertions(+), 88 deletions(-) create mode 100644 examples/issue-0002.sx create mode 100644 examples/test_union_array.sx diff --git a/examples/11-vector-math.sx b/examples/11-vector-math.sx index ee3c2e5..efd87fe 100644 --- a/examples/11-vector-math.sx +++ b/examples/11-vector-math.sx @@ -1,6 +1,22 @@ #import "modules/std.sx"; math :: #import "modules/math"; +dot :: (a: Vector(3,f32), b: Vector(3,f32)) -> f32 { + a.x*b.x + a.y*b.y + a.z*b.z; +} + +cross :: (a: Vector(3,f32), b: Vector(3,f32)) -> Vector(3,f32) { + .[a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x]; +} + +length :: (v: Vector(3,f32)) -> f32 { + math.sqrt(dot(v, v)); +} + +normalize :: (v: Vector(3,f32)) -> Vector(3,f32) { + v / length(v); +} + vec3 :: (x:f32, y:f32, z:f32) -> Vector(3,f32) { .[x, y, z]; } @@ -10,19 +26,19 @@ main :: () { b := vec3(0, 1, 0); // dot product - d := math.dot(a, b); + d := dot(a, b); print("dot: {}\n", d); // cross product - cr := math.cross(a, b); + cr := cross(a, b); print("cross: {}\n", cr); // length v := vec3(3, 4, 0); - len := math.length(v); + len := length(v); print("length: {}\n", len); // normalize - n := math.normalize(v); + n := normalize(v); print("norm: {}\n", n); } diff --git a/examples/15-while.sx b/examples/15-while.sx index cdf9049..a4b8246 100644 --- a/examples/15-while.sx +++ b/examples/15-while.sx @@ -12,7 +12,7 @@ sumOf10 :: () -> s32 { someSum :: #run sumOf10(); -main :: { +main :: () { // Basic while loop: count to 5 i := 0; while i < 5 { diff --git a/examples/22-anytype.sx b/examples/22-anytype.sx index 55cd709..a9db20a 100644 --- a/examples/22-anytype.sx +++ b/examples/22-anytype.sx @@ -1,6 +1,6 @@ #import "modules/std.sx"; -main :: { +main :: () { i := 0; while i < 10 { i+=1; diff --git a/examples/35-closures.sx b/examples/35-closures.sx index 825ee5c..5439a0c 100644 --- a/examples/35-closures.sx +++ b/examples/35-closures.sx @@ -31,6 +31,7 @@ main :: () { // 1. Basic closure with capture offset := 100; add_offset := closure((x: s64) -> s64 => x + offset); + print("basic: {}\n", add_offset(42)); // 2. Capture by value (snapshot semantics) diff --git a/examples/50-smoke.sx b/examples/50-smoke.sx index 5ad4bfe..3a84045 100644 --- a/examples/50-smoke.sx +++ b/examples/50-smoke.sx @@ -1,5 +1,5 @@ #import "modules/std.sx"; -#import "modules/math"; +#import "modules/math/math.sx"; pkg :: #import "modules/testpkg"; // ============================================================ @@ -277,7 +277,7 @@ SumBox :: struct ($T: Type/Summable) { } // ============================================================ -main :: { +main :: () { // ======================================================== // 1. LITERALS @@ -1977,7 +1977,6 @@ END; if x > 100 { return 100; } return x + offset; }); - // Workaround: assign result with explicit type so print wraps correctly r1 : s64 = clamp(10); r2 : s64 = clamp(0 - 5); r3 : s64 = clamp(999); diff --git a/examples/issue-0002.sx b/examples/issue-0002.sx new file mode 100644 index 0000000..2bdaa10 --- /dev/null +++ b/examples/issue-0002.sx @@ -0,0 +1,25 @@ +#import "modules/std.sx"; + +// Issue: nested field assignment through pointer +// self.inner.field = value should work when self is a pointer + +Inner :: struct { + len: s64; + cap: s64; +} + +Outer :: struct { + inner: Inner; + count: s64; + + reset :: (self: *Outer) { + self.inner.len = 0; // error: field assignment target must be a variable + self.count += 1; + } +} + +main :: () { + o := Outer.{ inner = Inner.{ len = 5, cap = 10 }, count = 0 }; + o.reset(); + print("{}\n", o.inner.len); +} diff --git a/examples/modules/testpkg/cwd_test.sx b/examples/modules/testpkg/cwd_test.sx index 020dede..f0a21ea 100644 --- a/examples/modules/testpkg/cwd_test.sx +++ b/examples/modules/testpkg/cwd_test.sx @@ -1,5 +1,4 @@ -// This file lives in modules/testpkg/ but imports modules/std.sx -// via cwd-relative path (not relative to this file's directory). -#import "modules/std.sx"; +// This file lives in modules/testpkg/ and imports std relative to its directory. +#import "../std.sx"; cwd_greet :: () -> string { format("cwd-import-ok"); } diff --git a/examples/test_union_array.sx b/examples/test_union_array.sx new file mode 100644 index 0000000..f72f8f1 --- /dev/null +++ b/examples/test_union_array.sx @@ -0,0 +1,14 @@ +#import "modules/std.sx"; + +Vec2 :: union { + data: [2]f32; + struct { x, y: f32; }; +} + +main :: () { + uv : Vec2 = ---; + uv.x = 1.0; + uv.y = 2.0; + print("promoted-x: {}\n", uv.x); + print("promoted-data0: {}\n", uv.data[0]); +} diff --git a/specs.md b/specs.md index 647fdb6..a86876f 100644 --- a/specs.md +++ b/specs.md @@ -951,9 +951,9 @@ main :: () { // void return, no -> annotation } -// Bare-block shorthand (equivalent to no-arg void function): -main :: { - // same as main :: () { ... } +// No-arg void function: +main :: () { + // ... } ``` @@ -1468,7 +1468,7 @@ compute :: (x: s32) -> s32 { Bare blocks can be used as statements to introduce a new lexical scope. Variables declared inside a scope block are local to that block. No trailing `;` is required. ```sx -main :: { +main :: () { x := 42; { x := 6; // shadows outer x diff --git a/src/codegen.zig b/src/codegen.zig index f611421..5f948fe 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -201,6 +201,8 @@ pub const CodeGen = struct { current_match_tags: ?[]const u64 = null, // Functions deferred to compile after all types are registered (e.g. any_to_string) deferred_fn_bodies: std.ArrayList(DeferredFn), + // AST nodes whose bodies were generated in Pass 4 (to avoid double generation in main) + generated_bodies: std.AutoHashMap(*const Node, void), // Libraries to link against (from #library directives) foreign_libraries: std.ArrayList([]const u8), // Set of foreign function names (for ABI lowering at call sites) @@ -436,6 +438,7 @@ pub const CodeGen = struct { .any_type_id_map = std.StringHashMap(u64).init(allocator), .any_type_entries = std.StringHashMap(AnyTypeEntry).init(allocator), .deferred_fn_bodies = std.ArrayList(DeferredFn).empty, + .generated_bodies = std.AutoHashMap(*const Node, void).init(allocator), .foreign_libraries = std.ArrayList([]const u8).empty, .foreign_fns = std.StringHashMap(void).init(allocator), .library_constants = std.StringHashMap([]const u8).init(allocator), @@ -1392,7 +1395,7 @@ pub const CodeGen = struct { switch (decl.data) { .fn_decl => |fd| { if (fd.body.data != .builtin_expr and fd.type_params.len == 0) { - try self.registerFnDecl(fd, fd.name); + _ = try self.registerFnDecl(fd, fd.name); } }, .struct_decl => |sd| try self.registerStructMethods(sd), @@ -1457,11 +1460,13 @@ pub const CodeGen = struct { } else { try self.genFnBody(fd, fd.name); } + try self.generated_bodies.put(decl, {}); } }, .const_decl => |cd| { if (cd.value.data == .lambda) { try self.genLambdaBody(cd.name, cd.value.data.lambda); + try self.generated_bodies.put(decl, {}); } }, .namespace_decl => |ns| { @@ -2338,7 +2343,7 @@ pub const CodeGen = struct { } } - fn registerFnDecl(self: *CodeGen, fd: ast.FnDecl, llvm_name: []const u8) !void { + fn registerFnDecl(self: *CodeGen, fd: ast.FnDecl, llvm_name: []const u8) !c.LLVMValueRef { const is_foreign = fd.body.data == .foreign_expr; // For foreign functions: resolve C symbol name (rename) and validate library ref const actual_llvm_name = if (is_foreign) blk: { @@ -2359,7 +2364,7 @@ pub const CodeGen = struct { } else llvm_name; const fn_type = try self.buildFnType(fd.params, fd.return_type, fd.name, is_foreign); const name_z = try self.allocator.dupeZ(u8, actual_llvm_name); - _ = c.LLVMAddFunction(self.module, name_z.ptr, fn_type); + const function = c.LLVMAddFunction(self.module, name_z.ptr, fn_type); // Track foreign functions for ABI lowering at call sites (use sx name for call-site lookup) if (is_foreign) { try self.foreign_fns.put(llvm_name, {}); @@ -2394,6 +2399,7 @@ pub const CodeGen = struct { break; } } + return function; } /// registerTypes helper: register type names within a namespace. @@ -2473,7 +2479,7 @@ pub const CodeGen = struct { } const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns.name, fd.name }); if (fd.body.data == .foreign_expr) { - try self.registerFnDecl(fd, fd.name); + _ = try self.registerFnDecl(fd, fd.name); try self.foreign_fns.put(qualified, {}); const fe = fd.body.data.foreign_expr; if (fe.c_name) |c_name| { @@ -2490,7 +2496,7 @@ pub const CodeGen = struct { } else if (fd.type_params.len > 0) { try self.generic_templates.put(qualified, fd); } else { - try self.registerFnDecl(fd, qualified); + _ = try self.registerFnDecl(fd, qualified); } }, .struct_decl => |sd| { @@ -2980,8 +2986,10 @@ pub const CodeGen = struct { if (ret_val) |val| { const prepared = try self.prepareReturnValue(val, ret_sx_type); self.ret(prepared); - } else { + } else if (ret_sx_type == .void_type) { self.retVoid(); + } else { + _ = c.LLVMBuildUnreachable(self.builder); } } } @@ -3014,30 +3022,45 @@ pub const CodeGen = struct { // Infer return type from body for => lambdas without explicit annotation const ret_sx_type = if (fd.return_type != null) self.resolveType(fd.return_type) else if (fd.is_arrow) self.inferType(fd.body) else Type.void_type; - // For arrow lambdas with inferred return type, build function manually - if (fd.is_arrow and fd.return_type == null) { - const ret_llvm_type = self.typeToLLVM(ret_sx_type); - var param_llvm_types = std.ArrayList(c.LLVMTypeRef).empty; - for (fd.params) |param| { - try param_llvm_types.append(self.allocator, self.typeToLLVM(self.resolveType(param.type_expr))); + // Build or register the LLVM function, keeping a direct reference + // (LLVMGetNamedFunction returns the first fn with that name, which + // may differ when multiple local functions share a name) + const function = blk: { + if (fd.is_arrow and fd.return_type == null) { + const ret_llvm_type = self.typeToLLVM(ret_sx_type); + var param_llvm_types = std.ArrayList(c.LLVMTypeRef).empty; + for (fd.params) |param| { + try param_llvm_types.append(self.allocator, self.typeToLLVM(self.resolveType(param.type_expr))); + } + const params_slice = try param_llvm_types.toOwnedSlice(self.allocator); + const fn_type = c.LLVMFunctionType( + ret_llvm_type, + if (params_slice.len > 0) params_slice.ptr else null, + @intCast(params_slice.len), + 0, + ); + const name_z2 = try self.allocator.dupeZ(u8, fd.name); + const func = c.LLVMAddFunction(self.module, name_z2.ptr, fn_type); + try self.function_return_types.put(fd.name, ret_sx_type); + break :blk func; + } else { + break :blk try self.registerFnDecl(fd, fd.name); } - const params_slice = try param_llvm_types.toOwnedSlice(self.allocator); - const fn_type = c.LLVMFunctionType( - ret_llvm_type, - if (params_slice.len > 0) params_slice.ptr else null, - @intCast(params_slice.len), - 0, - ); - const name_z2 = try self.allocator.dupeZ(u8, fd.name); - _ = c.LLVMAddFunction(self.module, name_z2.ptr, fn_type); - try self.function_return_types.put(fd.name, ret_sx_type); - } else { - try self.registerFnDecl(fd, fd.name); + }; + + // Skip if this exact AST node was already generated in Pass 4 + // (top-level fn_decls appear both in Pass 4 and main's body) + if (self.generated_bodies.contains(node)) { + self.named_values.deinit(); + self.named_values = saved_named; + self.narrowed_types = saved_narrowed; + self.current_return_type = saved_ret; + self.current_function = saved_fn; + self.positionAt(saved_bb); + return null; } + self.current_return_type = ret_sx_type; - const name_z = try self.allocator.dupeZ(u8, fd.name); - const function = c.LLVMGetNamedFunction(self.module, name_z.ptr) orelse - return self.emitErrorFmt("local function '{s}' not found", .{fd.name}); self.current_function = function; _ = self.appendBlock(function, "entry"); @@ -3071,7 +3094,7 @@ pub const CodeGen = struct { const ret_val = try self.prepareReturnValue(val, ret_sx_type); self.ret(ret_val); } else { - self.retVoid(); + _ = c.LLVMBuildUnreachable(self.builder); } } @@ -3081,6 +3104,25 @@ pub const CodeGen = struct { self.current_return_type = saved_ret; self.current_function = saved_fn; self.positionAt(saved_bb); + + // Register local function in outer scope's named_values so it + // shadows any top-level function with the same name. + { + var param_types_list = std.ArrayList(Type).empty; + for (fd.params) |param| { + try param_types_list.append(self.allocator, self.resolveType(param.type_expr)); + } + const ret_type_ptr = try self.allocator.create(Type); + ret_type_ptr.* = ret_sx_type; + const fn_ty: Type = .{ .function_type = .{ + .param_types = try param_types_list.toOwnedSlice(self.allocator), + .return_type = ret_type_ptr, + } }; + const local_name_z = try self.allocator.dupeZ(u8, fd.name); + const fn_alloca = self.buildEntryBlockAlloca(self.ptrType(), local_name_z.ptr); + _ = c.LLVMBuildStore(self.builder, function, fn_alloca); + try self.named_values.put(fd.name, .{ .ptr = fn_alloca, .ty = fn_ty }); + } } return null; }, @@ -4841,7 +4883,7 @@ pub const CodeGen = struct { try self.generic_templates.put(qualified, fd); } else { // Non-generic struct, non-generic method: register directly - try self.registerFnDecl(fd, qualified); + _ = try self.registerFnDecl(fd, qualified); } } } @@ -4851,6 +4893,9 @@ pub const CodeGen = struct { fn registerProtocolDecl(self: *CodeGen, pd: ast.ProtocolDecl) !void { try self.protocol_decls.put(pd.name, pd); + // Skip if already registered (can happen with diamond imports) + if (self.type_registry.contains(pd.name)) return; + if (pd.is_inline) { // #inline protocol: generate struct { ctx: *void, method1: fn_ptr, method2: fn_ptr, ... } const n_fields = 1 + pd.methods.len; // ctx + one fn-ptr per method @@ -5188,7 +5233,7 @@ pub const CodeGen = struct { try self.generic_templates.put(qualified, fd); } else { // Non-generic: register directly - try self.registerFnDecl(fd, qualified); + _ = try self.registerFnDecl(fd, qualified); } try self.fn_signatures.put(qualified, self.buildFnSignature(fd)); } @@ -5211,7 +5256,7 @@ pub const CodeGen = struct { // Synthesize a fn_decl: method_name :: (self: *ConcreteType, params...) -> R { default_body } const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ib.target_type, method.name }); const self_fd = try self.synthesizeDefaultMethod(ib.target_type, method); - try self.registerFnDecl(self_fd, qualified); + _ = try self.registerFnDecl(self_fd, qualified); try self.fn_signatures.put(qualified, self.buildFnSignature(self_fd)); } } @@ -7931,30 +7976,6 @@ pub const CodeGen = struct { return self.genCallByName(resolved, call_node); } - // Check if this is a generic function call - if (self.generic_templates.get(callee_name)) |template| { - return self.genGenericCall(callee_name, template, call_node); - } - // Intra-namespace fallback for generic templates - if (self.current_namespace) |ns| { - const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns, callee_name }); - if (self.generic_templates.get(qualified)) |template| { - return self.genGenericCall(qualified, template, call_node); - } - } - - // Check for #builtin function (only available when imported) - if (self.builtin_functions.contains(callee_name)) { - return self.dispatchBuiltin(callee_name, call_node); - } - // Intra-namespace fallback for builtins - if (self.current_namespace) |ns| { - const qualified_builtin = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns, callee_name }); - if (self.builtin_functions.contains(qualified_builtin)) { - return self.dispatchBuiltin(qualified_builtin, call_node); - } - } - // Compiler intrinsics (always available, no #builtin declaration needed) if (std.mem.eql(u8, callee_name, "sqrt")) { return self.genMathIntrinsic(call_node, "sqrt"); @@ -7981,7 +8002,8 @@ pub const CodeGen = struct { return self.genClosureIntrinsic(call_node); } - // Local variable takes priority: closures and function pointers shadow LLVM named functions + // Local variables shadow imported functions: closures and function pointers + // take priority over generic templates, builtins, and LLVM named functions. if (self.lookupValue(callee_name)) |v| { const entry = v.asNamedValue(); if (entry) |e| { @@ -7994,6 +8016,30 @@ pub const CodeGen = struct { } } + // Check if this is a generic function call + if (self.generic_templates.get(callee_name)) |template| { + return self.genGenericCall(callee_name, template, call_node); + } + // Intra-namespace fallback for generic templates + if (self.current_namespace) |ns| { + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns, callee_name }); + if (self.generic_templates.get(qualified)) |template| { + return self.genGenericCall(qualified, template, call_node); + } + } + + // Check for #builtin function (only available when imported) + if (self.builtin_functions.contains(callee_name)) { + return self.dispatchBuiltin(callee_name, call_node); + } + // Intra-namespace fallback for builtins + if (self.current_namespace) |ns| { + const qualified_builtin = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns, callee_name }); + if (self.builtin_functions.contains(qualified_builtin)) { + return self.dispatchBuiltin(qualified_builtin, call_node); + } + } + var nbuf: [256]u8 = undefined; var callee_fn = c.LLVMGetNamedFunction(self.module, self.nameToCStr(callee_name, &nbuf)); // Foreign function fallback: qualified name "ns.Func" → try unqualified "Func" (the C symbol) @@ -8417,7 +8463,23 @@ pub const CodeGen = struct { if (ret_llvm == self.voidType()) { _ = c.LLVMBuildRetVoid(self.builder); } else { - _ = c.LLVMBuildRet(self.builder, result); + // Convert result type if it doesn't match the thunk's declared return type + var ret_val = result; + const result_ty = c.LLVMTypeOf(result); + if (result_ty != ret_llvm) { + const src_kind = c.LLVMGetTypeKind(result_ty); + const dst_kind = c.LLVMGetTypeKind(ret_llvm); + if (src_kind == c.LLVMIntegerTypeKind and dst_kind == c.LLVMIntegerTypeKind) { + const src_bits = c.LLVMGetIntTypeWidth(result_ty); + const dst_bits = c.LLVMGetIntTypeWidth(ret_llvm); + if (src_bits > dst_bits) { + ret_val = c.LLVMBuildTrunc(self.builder, result, ret_llvm, "thunk_trunc"); + } else { + ret_val = c.LLVMBuildSExt(self.builder, result, ret_llvm, "thunk_sext"); + } + } + } + _ = c.LLVMBuildRet(self.builder, ret_val); } // Restore position diff --git a/src/lsp/server.zig b/src/lsp/server.zig index b8d9af9..9c08f9b 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -1084,7 +1084,7 @@ pub const Server = struct { }; var hints = std.ArrayList(lsp.InlayHint).empty; - collectInlayHints(self.allocator, root, sema.symbols, doc.source, &hints); + collectInlayHints(self.allocator, root, sema.symbols, sema.fn_signatures, doc.source, &hints); self.collectCallHints(doc, root, &hints); const result_json = try lsp.inlayHintsJson(self.allocator, hints.items); try self.sendResponse(id_json, result_json); @@ -1094,37 +1094,46 @@ pub const Server = struct { allocator: std.mem.Allocator, node: *const sx.ast.Node, symbols: []const sx.sema.Symbol, + fn_signatures: std.StringHashMap(sx.sema.FnSignature), source: [:0]const u8, hints: *std.ArrayList(lsp.InlayHint), ) void { switch (node.data) { .root => |r| { - for (r.decls) |decl| collectInlayHints(allocator, decl, symbols, source, hints); + for (r.decls) |decl| collectInlayHints(allocator, decl, symbols, fn_signatures, source, hints); }, .block => |b| { - for (b.stmts) |stmt| collectInlayHints(allocator, stmt, symbols, source, hints); + for (b.stmts) |stmt| collectInlayHints(allocator, stmt, symbols, fn_signatures, source, hints); }, .fn_decl => |fd| { - collectInlayHints(allocator, fd.body, symbols, source, hints); + collectInlayHints(allocator, fd.body, symbols, fn_signatures, source, hints); + // Return type hint for arrow functions without explicit return type + if (fd.return_type == null and fd.is_arrow) { + if (fn_signatures.get(fd.name)) |sig| { + if (sig.return_type != .void_type) { + addReturnTypeHint(allocator, node.span, source, sig.return_type, hints); + } + } + } }, .lambda => |lm| { - collectInlayHints(allocator, lm.body, symbols, source, hints); + collectInlayHints(allocator, lm.body, symbols, fn_signatures, source, hints); }, .if_expr => |ie| { if (ie.binding_name) |bname| { addBindingHint(allocator, bname, node.span, symbols, source, hints); } - collectInlayHints(allocator, ie.then_branch, symbols, source, hints); - if (ie.else_branch) |eb| collectInlayHints(allocator, eb, symbols, source, hints); + collectInlayHints(allocator, ie.then_branch, symbols, fn_signatures, source, hints); + if (ie.else_branch) |eb| collectInlayHints(allocator, eb, symbols, fn_signatures, source, hints); }, .while_expr => |we| { if (we.binding_name) |bname| { addBindingHint(allocator, bname, node.span, symbols, source, hints); } - collectInlayHints(allocator, we.body, symbols, source, hints); + collectInlayHints(allocator, we.body, symbols, fn_signatures, source, hints); }, .for_expr => |fe| { - collectInlayHints(allocator, fe.body, symbols, source, hints); + collectInlayHints(allocator, fe.body, symbols, fn_signatures, source, hints); }, .var_decl => |vd| { // Only show hint when type is inferred (:= syntax) @@ -1135,9 +1144,22 @@ pub const Server = struct { .const_decl => |cd| { // Skip if explicit type annotation if (cd.type_annotation != null) return; + // Handle lambda with return type hint + if (cd.value.data == .lambda) { + const lam = cd.value.data.lambda; + collectInlayHints(allocator, lam.body, symbols, fn_signatures, source, hints); + if (lam.return_type == null) { + if (fn_signatures.get(cd.name)) |sig| { + if (sig.return_type != .void_type) { + addReturnTypeHint(allocator, cd.value.span, source, sig.return_type, hints); + } + } + } + return; + } // Skip functions, types, structs, enums, unions, comptime, foreign, library switch (cd.value.data) { - .lambda, .fn_decl, .type_expr, .struct_decl, .enum_decl, .union_decl, + .fn_decl, .type_expr, .struct_decl, .enum_decl, .union_decl, .comptime_expr, .foreign_expr, .library_decl, => return, else => {}, @@ -1241,6 +1263,47 @@ pub const Server = struct { } } + fn addReturnTypeHint( + allocator: std.mem.Allocator, + span: sx.ast.Span, + source: [:0]const u8, + return_type: sx.types.Type, + hints: *std.ArrayList(lsp.InlayHint), + ) void { + // Find '(' from span start + var pos: u32 = span.start; + while (pos < source.len and source[pos] != '(') : (pos += 1) {} + if (pos >= source.len) return; + + // Match nested parens to find closing ')' + var depth: u32 = 0; + while (pos < source.len) : (pos += 1) { + if (source[pos] == '(') { + depth += 1; + } else if (source[pos] == ')') { + depth -= 1; + if (depth == 0) break; + } + } + if (pos >= source.len or depth != 0) return; + + // Place hint right after ')' + const loc = sx.errors.SourceLoc.compute(source, pos + 1); + if (loc.line == 0 or loc.col == 0) return; + + const type_name = return_type.displayName(allocator) catch return; + const label = std.fmt.allocPrint(allocator, "-> {s}", .{type_name}) catch return; + + hints.append(allocator, .{ + .line = loc.line - 1, + .character = loc.col - 1, + .label = label, + .kind = 1, + .padding_left = true, + .padding_right = true, + }) catch {}; + } + fn findSymbolAtSpan(symbols: []const sx.sema.Symbol, span_start: u32, name: []const u8) ?sx.sema.Symbol { for (symbols) |sym| { if (sym.def_span.start == span_start and std.mem.eql(u8, sym.name, name)) { diff --git a/src/sema.zig b/src/sema.zig index 2a0c9a0..6f1a928 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -129,7 +129,8 @@ pub const Analyzer = struct { fn registerTopLevelDeclPrefixed(self: *Analyzer, node: *Node, ns_prefix: ?[]const u8) !void { switch (node.data) { .fn_decl => |fd| { - const ret_ty = resolveReturnType(fd); + const ret_ty = resolveReturnType(fd) orelse + if (fd.is_arrow) self.inferFnReturnType(fd.params, fd.body) else null; try self.addSymbol(fd.name, .function, ret_ty, node.span); // Populate fn_signatures registry var param_types = std.ArrayList(Type).empty; @@ -171,7 +172,10 @@ pub const Analyzer = struct { const pt = Type.fromTypeExpr(param.type_expr) orelse Type.s(64); try param_types.append(self.allocator, pt); } - const ret = if (lam.return_type) |rt| Type.fromTypeExpr(rt) orelse .void_type else .void_type; + const ret = if (lam.return_type) |rt| + Type.fromTypeExpr(rt) orelse .void_type + else + self.inferFnReturnType(lam.params, lam.body) orelse .void_type; const key = if (ns_prefix) |pfx| try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ pfx, cd.name }) else @@ -666,7 +670,21 @@ pub const Analyzer = struct { fn analyzeNode(self: *Analyzer, node: *Node) !void { switch (node.data) { .fn_decl => |fd| { - try self.addSymbol(fd.name, .function, resolveReturnType(fd), node.span); + const local_ret_ty = resolveReturnType(fd) orelse + if (fd.is_arrow) self.inferFnReturnType(fd.params, fd.body) else null; + try self.addSymbol(fd.name, .function, local_ret_ty, node.span); + // Register fn_signatures for local functions (for return type hints + hover) + { + var param_types = std.ArrayList(Type).empty; + for (fd.params) |param| { + const pt = Type.fromTypeExpr(param.type_expr) orelse Type.s(64); + try param_types.append(self.allocator, pt); + } + try self.fn_signatures.put(fd.name, .{ + .param_types = try param_types.toOwnedSlice(self.allocator), + .return_type = local_ret_ty orelse .void_type, + }); + } try self.pushScope(); try self.analyzeParams(fd.params); try self.analyzeNode(fd.body); @@ -967,6 +985,26 @@ pub const Analyzer = struct { return null; } + /// Infer return type from a function/lambda body by temporarily registering params. + fn inferFnReturnType(self: *Analyzer, params: []const ast.Param, body: *const Node) ?Type { + self.pushScope() catch return null; + for (params) |param| { + const pt = Type.fromTypeExpr(param.type_expr) orelse Type.s(64); + self.addSymbol(param.name, .param, pt, param.name_span) catch {}; + } + // Arrow fn_decl wraps body in block{[expr]} — unwrap to inner expression + const expr_node = if (body.data == .block) blk: { + const stmts = body.data.block.stmts; + if (stmts.len > 0) break :blk stmts[stmts.len - 1]; + break :blk body; + } else body; + + const inferred = self.inferExprType(expr_node); + self.popScope(); + if (inferred != .void_type) return inferred; + return null; + } + fn resolveTypeAnnotation(self: *Analyzer, type_node: ?*Node) ?Type { if (type_node) |tn| { if (Type.fromTypeExpr(tn)) |t| return t; diff --git a/tests/expected/50-smoke.txt b/tests/expected/50-smoke.txt index 9237778..29dfc26 100644 --- a/tests/expected/50-smoke.txt +++ b/tests/expected/50-smoke.txt @@ -1,2 +1,564 @@ -/Volumes/Store/dev/swipelab/sx/examples/50-smoke.sx:5:48: error: cannot read import 'modules/std.sx' (not a file or directory) -/Volumes/Store/dev/swipelab/sx/examples/50-smoke.sx:3:1: error: cannot read import '/Volumes/Store/dev/swipelab/sx/examples/modules/testpkg' (not a file or directory) +=== 1. Literals === +decimal: 42 +hex: 255 +binary: 10 +float: 3.140000 +f64: 2.718281 +true: true +false: false +escapes: hello world +multiline: line1 +line2 +heredoc: raw heredoc + +undef-then-set: 77 +enum-lit: .green +null-ptr: null +string-len: 5 +empty-string: 0 +=== 2. Operators === +add: 7 +sub: 7 +mul: 42 +div: 5 +mod: 2 +neg: -5 +eq: true +neq: true +lt: true +gt: true +le: true +ge: true +chain: true +chain-gt: true +chain-mixed: true +eq-chain: true +eq-chain-f: false +band: 15 +bor: 7 +bxor: 240 +bxor2: 5 +bnot: -1 +bnot2: -2 +shl: 16 +shr: 16 +shl2: 24 +shr2: 127 +band-var: 15 +bor-var: 7 +bxor-var: 240 +shl-var: 16 +shr-var: 15 +bnot-var: -16 +and-assign: 15 +or-assign: 255 +xor-assign: 240 +shl-assign: 256 +shr-assign: 16 +mod-var: 2 +and: true +and-false: false +or: true +or-false: false +short-and: false +short-or: true +ca+=: 15 +ca-=: 12 +ca*=: 24 +ca/=: 4 +prec1: 14 +prec2: 20 +xx-cast: 200 +widen-u8-s64: 200 +widen-s32-f64: 42.000000 +widen-f32-f64: 1.500000 +widen-u8-s16: 100 +xx-s64-s32: 12345 +xx-f64-f32: 1.500000 +xx-f64-s32: 7 +=== 3. Types === +s8: 127 +s16: 32000 +s32: 100000 +u8: 255 +u16: 65000 +u32: 4000000 +alias: 1.500000 +struct-pos: Point{x: 1, y: 2} +struct-prefix: Point{x: 3, y: 4} +struct-named: Point{x: 20, y: 10} +struct-shorthand: Point{x: 5, y: 6} +defaults: a=0 b=99 +field-assign: Point{x: 42, y: 99} +enum: .red +enum-eq: true +enum-neq: true +backing: .err +tagged: .circle(3.140000) +payload: 3.140000 +void-variant: .none +reassign: .circle(1.000000) +reassign2: .rect(Shape.rect{w: 5.000000, h: 3.000000}) +enum-prefix: .circle(2.500000) +match: rect +match-expr: 10 +match-expr-else: 99 +capture: 9.500000 +capture-arrow: 7.500000 +else-match: other +int-match: two +int-match-else: unknown +bool-match-t: yes +bool-match-f: no +bool: true +union-f: 3.140000 +union-i: 1078523331 +promoted-x: 1.000000 +promoted-data0: 1.000000 +arr[2]: 30 +arr.len: 5 +arr-assign: [1, 99, 3] +sl[0]: 1 +sl.len: 5 +sl-assign: [10, 55, 0] +sub: [20, 30, 40] +head: [10, 20, 30] +tail: [30, 40, 50] +slice-of-slice: [20, 30] +strsub: world +str-prefix: hello +str-suffix: world +deref: Point{x: 10, y: 20} +auto-deref: 10 +mp[0]: 10 +mp[3]: 40 +mp-write: 99 +ptr==null: true +ptr!=null: false +ptr2==null: false +ptr2!=null: true +vec-construct: [1.000000, 3.000000, 2.000000] +vec-add: [5.000000, 7.000000, 9.000000] +vec-sub: [4.000000, 3.000000, 2.000000] +vec-mul: [2.000000, 6.000000, 12.000000] +vec-div: [5.000000, 3.000000, 2.000000] +vec-scalar: [2.000000, 6.000000, 4.000000] +vec-neg: [-1.000000, -3.000000, -2.000000] +vec-x: 10.000000 +vec-y: 20.000000 +vec-z: 30.000000 +vec-idx: 20.000000 +=== 4. Control Flow === +ite: 1 +ite-both: 10 20 +if-block: yes +if-no-else: after +nested-if: deep +if-else-if: second +if-block-expr: 15 +while: 5 +while-false: skipped +while-break: 7 +while-continue: 25 +while-sum: 55 +nested-while: 9 +nested-break: 2 2 +for: 10 20 30 40 +for-print: 10 20 30 40 +for-idx: 0 1 2 3 +for-2arg: 10@0 20@1 30@2 40@3 +for-break: 10 20 +for-continue: 10 30 40 +for-slice: 10 20 30 +for-slice-idx: 0:10 1:20 2:30 +for-nested: (0,0) (0,1) (1,0) (1,1) +for-break-idx: 2 +multi: 1 2 3 +=== 5. Functions === +const: 42 +typed-const: 3.140000 +default-init: 0 +implicit-ret: 42 +early-ret: 5 +early-ret2: 99 +void-return: ok +generic-s32: 42 +generic-f32: 1.500000 +generic-bool: true +generic-multi: 30 +lambda: 14 +lambda-ret: 5.000000 +local-fn: 7 +fn-nested: 26 +varargs: 15 +spread: 60 +fp: 7 +fp-reassign: 12 +fp-apply: 30 +=== 6. Scoping === +inner: 200 +outer: 100 +shadow-type: 42 +shadow-type: 3.140000 +nest3: 3 +nest2: 2 +nest1: 1 +scope-isolate: 100 +scope-reuse: 1 +scope-reuse: 2 +scope-reuse: 1 +defer-a +defer-b +defer-c +d4 +d3 +d2 +d1 +inner-defer +outer-defer +defer-in-if: body +defer-in-if: deferred +=== 7. Builtins === +out-ok +sqrt: 3.000000 +sqrt-f64: 4.000000 +sizeof-s32: 4 +sizeof-f64: 8 +sizeof-struct: 8 +typeof: int +typeof-float: float +typeof-string: string +typeof-bool: bool +typeof-struct: struct +typeof-enum: enum +typename: Point +fieldcount: 2 +fieldcount-enum: 3 +fieldname0: x +fieldname1: y +fieldname-enum0: red +fieldname-enum2: blue +fieldval0: 11 +fieldval1: 22 +fieldidx: 1 +fieldidx-tagged: 0 +fieldidx-tagged2: 2 +cast: 3 +cast-int-f64: 42.000000 +=== 8. Comptime === +run-const: 25 +run-expr: 42 +run-chain: 30 +ct-opt-coalesce: 141 +ct-opt-unwrap: 77 +ct-opt-guard: 10 +insert-ok +insert-gen: 42 +=== 9. Flags === +flags: .read | .write +has-read: yes +flags-neg: no-read +flags-single: .execute +flags-all: .read | .write | .execute +flags-raw: 3 +flags-explicit: .vsync | .resizable +flags-explicit-raw: 68 +--- swap --- +var swap: 20 10 +arr swap: 3 1 +3-way: 3 1 2 +=== 15. Foreign === +foreign-rename: 42 +=== 16. Compound Assign === +f64+=f32: 13.000000 +s64-=s32: 93 +=== 17. Slice Ptr === +sl-ptr[0]: 20 +sl-ptr[1]: 30 +=== 18. Array of Structs === +arr-struct-x: 3 +for-struct: Point{x: 1, y: 2} +for-struct: Point{x: 3, y: 4} +=== 19. Local Fn Return === +local-struct: 42 99 +local-enum: .circle(2.500000) +=== 20. UFCS Return Type === +direct: 7 +ufcs: 7 +=== 21. Type-Named Vars === +s2: 42 +s2+1: 43 +=== 22. If-Struct === +if-struct: 10 20 +else-struct: 30 40 +=== 23. Nested Arrays === +m[0][0]: 1 +m[0][2]: 3 +m[1][0]: 4 +m[1][2]: 6 +=== 24. String Comparison === +str-eq: true +str-neq: true +str-diff: false +empty-eq: true +=== 25. Array Loop Mutation === +loop-fill: 1 2 3 4 +compound: 13 +=== 26. #using === +using-x: 1 +using-y: 2 +using-z: 3 +pkt-id: 10 +pkt-ver: 42 +pkt-pay: 99 +sprite-px: 10 +sprite-r: 255 +sprite-scale: 1 +say: hello (len=5) +n=42 +=== Tuples === +40 +2 +10 +10 +42 +0 +0 +=== UFCS Aliases === +42 +42 +42 +42 +42 +3 +3 +3 +2 +1 +99 +=== Tuple Operators === +true +false +true +false +1 +2 +3 +4 +1 +2 +1 +2 +1 +2 +true +false +false +true +true +true +true +false +--- directory imports --- +7 +30 +hello from testpkg +cwd-import-ok +--- pipe operator --- +7 +30 +14 +hello world +piped ok! +alloc len: 5 +alloc[0]: 10 +alloc[4]: 50 +bytes len: 3 +--- allocators --- +gpa allocs: 2 +gpa final: 0 +arena chunks: 1 +arena overflow: 2 +arena a1: 42 +arena a3: 99 +arena reset idx: 0 +arena reset gpa: 1 +arena deinit: 0 +buf pos: 48 +buf overflow: 0 +buf reset: 0 +1 == (1) +(1) == 1 +1 == 1 +--- optionals --- +opt x: 42 +opt y: null +unwrap: 10 +coalesce a: 42 +coalesce b: 99 +if-bind x: 7 +if-bind y: none +match some: 55 +match none: 0 +wrap pos: 5 +wrap neg: null +opt field default: null +opt field set: 42 +opt param a: 42 +opt param b: 0 +opt param 7: 7 +generic opt 1: 5 +generic opt 2: 7 +generic opt 3: null +chain some: 10 +chain none: 0 +chain print: 20 +chain null: null +deep chain 1: 99 +deep chain 2: 0 +narrow x: 42 +narrow y else: null +narrow else x: 42 +guard some: 42 +guard none: 0 +and both: 10 20 +and one null +or guard: 7 +or guard null: 0 +nested narrow: 10 20 +guard loop: 3 +block-lambda: 50 +block-lambda: 0 +block-lambda: 100 +hello block +named-fn-type: 7 +xx-fnptr: 142 +closure-type: fn_ptr-nonnull=true +closure-type: env-null=true +closure-call: 15 +auto-promote: 20 +auto-promote-var: 10 +closure-capture: 52 +closure-snapshot: 15 +closure-nocap: 14 +closure-multi: 33 +closure-block: 60 +closure-block: 0 +closure-block: 100 +[LOG] hello +closure-hof: 30 +closure-hof-bare: 20 +closure-f32: 10.000000 +closure-bool: hello +closure-2p: 107 +closure-3p: 61 +closure-mix: Alice is 35 +closure-rbool: false true +closure-reduce: 115 +closure-factory: 105 110 +closure-struct: 10 20 +closure-compose: 30 +closure-indep: 15 50 +opt-closure: none +opt-closure: 15 +opt-closure-btn: 1 99 +opt-closure-btn: null +closure-ptr: 3 +closure-enum: 2 +closure-rstr: [INFO] ok +closure-rstruct: 11 22 +closure-linear: 37 +closure-clamp: 0 100 255 +closure-compose2: 12 +closure-chain: 22 +closure-map: 3 6 9 12 15 +closure-filter: 3 [3 4 5] +closure-sort: 5 4 3 2 1 +closure-fe: item 0=10 +closure-fe: item 1=20 +closure-fe: item 2=30 +closure-find: 2 +closure-any: false true +closure-struct-field: -5 +closure-btn: 1 99 +closure-counter: 1 2 3 +closure-acc: 105 115 +closure-loop: 115 +closure-reassign: 11 +closure-reassign: 20 +closure-snapstruct: 15 +closure-cap-promoted: 11 +closure-iife: 15 +closure-toggle: none +closure-toggle: true +closure-panel: main 800x600 +closure-chain-call: true +closure-loop-0: 1 +closure-loop-1: 11 +closure-loop-4: 41 +closure-cond: 10 +closure-form: submitted +closure-form: no cancel +closure-null-env: true +closure-slice: 10 20 30 +closure-arena: 15 +closure-gpa: 17 allocs=0 +closure-opt: 42 +closure-ropt: 50 +closure-ropt: none +closure-mixed: 10 +closure-mixed: 15 +closure-mixed: 25 +closure-factory-indep: 20 30 40 +closure-deep-chain: 122 +closure-8cap: 36 +closure-4param: 10 +closure-shared-ptr: 7 +closure-f64: true +closure-zerocap: 49 true +closure-struct-method: 7 +closure-multi-factory: 10 +closure-multi-factory: 20 +closure-multi-factory: 30 +closure-bool-cap: true false +closure-as-arg: 142 +closure-strfmt: hello world +closure-ptr-before: 10 +closure-ptr-after: 42 +closure-neg: -70 +closure-proto-cap: true +closure-chain-factory: 37 +closure-while-cond: 3 +closure-infer: 7 +closure-infer-arg: 15 +closure-infer-block: 12 +closure-infer-cap: 105 +closure-infer-factory: 35 +closure-infer-compose: 11 +closure-infer-void: 42 +=== Protocols === +P1.1: 3 +P1.2: 30 +P2.1: 42 +P2.2: 150 +P2.3: 5 10 +P3.1: 5 +P3.2: 12 +hi hi +P4.1: 2 +yo yo +P4.2: 2 +P4.3: 6 2 +P5.1: true false +P5.2: 10 20 +P5.5: true false +P5.3: true false +P6.1: true false +P6.2: true false +P6.3: true false +P6.4: 40 +P6.5: 20 +P7.1: 30 +P7.2: 10 300 +P2.6: 5 10 +=== DONE ===