lsp/sema: clearer message for 'context' inside a callconv(.c) function

Now that 'context' resolves as an implicit global, accessing it inside a callconv(.c) function (an FFI callback/trampoline) would silently resolve — but the C ABI carries no implicit context parameter, so it's actually unavailable there. Sema now tracks the current function's calling convention and, for 'context' under callconv(.c), emits a specific diagnostic ('unavailable in a callconv(.c) function — pass what you need explicitly') instead of resolving it or saying 'undefined variable'.
This commit is contained in:
agra
2026-05-31 12:19:02 +03:00
parent 847468938b
commit 9a9198511d

View File

@@ -79,6 +79,9 @@ pub const Analyzer = struct {
member_refs: std.ArrayList(MemberRef),
/// Source text — lets `spanOf` map an AST string slice back to a Span.
source: []const u8 = "",
/// True while analysing a `callconv(.c)` function body — the C ABI carries
/// no implicit `context` parameter, so `context` is unavailable there.
in_c_conv: bool = false,
diagnostics: std.ArrayList(Diagnostic),
scope_depth: u32,
/// Stack of symbol counts at each scope entry, for popScope cleanup.
@@ -703,10 +706,13 @@ pub const Analyzer = struct {
fn analyzeTopLevelDecl(self: *Analyzer, node: *Node) !void {
switch (node.data) {
.fn_decl => |fd| {
const saved_cc = self.in_c_conv;
self.in_c_conv = fd.call_conv == .c;
try self.pushScope();
try self.analyzeParams(fd.params);
try self.analyzeNode(fd.body);
self.popScope();
self.in_c_conv = saved_cc;
},
.const_decl => |cd| {
try self.analyzeNode(cd.value);
@@ -722,10 +728,13 @@ pub const Analyzer = struct {
for (sd.methods) |mnode| {
if (mnode.data == .fn_decl) {
const m = mnode.data.fn_decl;
const saved_cc = self.in_c_conv;
self.in_c_conv = m.call_conv == .c;
try self.pushScope();
try self.analyzeParams(m.params);
try self.analyzeNode(m.body);
self.popScope();
self.in_c_conv = saved_cc;
}
}
},
@@ -843,6 +852,14 @@ pub const Analyzer = struct {
}
fn resolveIdentifier(self: *Analyzer, name: []const u8, span: Span) !void {
if (self.in_c_conv and std.mem.eql(u8, name, "context")) {
try self.diagnostics.append(self.allocator, .{
.level = .warn,
.span = span,
.message = "`context` is unavailable in a `callconv(.c)` function — the C ABI has no implicit context parameter; pass what you need explicitly",
});
return;
}
// Use symbol index for O(1) name lookup, then walk backwards through indices
if (self.symbol_index.get(name)) |indices| {
var j = indices.items.len;
@@ -891,10 +908,13 @@ pub const Analyzer = struct {
.return_type = local_ret_ty orelse .void_type,
});
}
const saved_cc = self.in_c_conv;
self.in_c_conv = fd.call_conv == .c;
try self.pushScope();
try self.analyzeParams(fd.params);
try self.analyzeNode(fd.body);
self.popScope();
self.in_c_conv = saved_cc;
},
.block => |blk| {
try self.pushScope();
@@ -2038,6 +2058,32 @@ test "sema: member references record fields, methods, and enum variants" {
try std.testing.expect(red_use);
}
test "sema: context in a callconv(.c) function reports a specific diagnostic" {
const parser_mod = @import("parser.zig");
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const source =
"Context :: struct { allocator: s64; data: s64; }" ++
"cb :: () -> s64 callconv(.c) { context; 0; }" ++
"ok :: () -> s64 { context; 0; }";
var parser = parser_mod.Parser.init(alloc, source);
const root = try parser.parse();
var an = Analyzer.init(alloc);
an.source = source;
const res = try an.analyze(root);
var c_conv_diag = false;
var undefined_diag = false;
for (res.diagnostics) |d| {
if (std.mem.indexOf(u8, d.message, "callconv(.c)") != null) c_conv_diag = true;
if (std.mem.indexOf(u8, d.message, "undefined") != null) undefined_diag = true;
}
try std.testing.expect(c_conv_diag); // `cb` accesses context under the C ABI
try std.testing.expect(!undefined_diag); // `ok`'s context resolves cleanly
}
test "sema: variable shadowing in same scope is allowed" {
const parser_mod = @import("parser.zig");