feat(C3.1): #foreign refs are validated — must name a #library or a named #import c unit

validateForeignRefs walks the merged tree (libraries + named c units,
nested namespaces included) and diagnoses any #foreign whose ref names
neither — a typo'd ref previously compiled and resolved silently
through whatever image carried the symbol. Decls synthesized from
#include headers carry no ref and are exempt. Flips the C0.2b pin;
zero collateral across the 608 other examples.
This commit is contained in:
agra
2026-06-12 17:09:07 +03:00
parent 0bd8f3e5ce
commit 1bad29e72e
6 changed files with 70 additions and 9 deletions

View File

@@ -235,6 +235,58 @@ pub fn processCImport(
// Native C compilation (compile to .o, not LLVM module)
// ---------------------------------------------------------------------------
// ── #foreign ref validation ───────────────────────────────────────────
fn collectForeignRefTargets(valid: *std.StringHashMap(void), decls: []const *Node) !void {
for (decls) |d| {
switch (d.data) {
.library_decl => |ld| try valid.put(ld.name, {}),
.namespace_decl => |ns| {
// A NAMED `#import c` unit lowers to a namespace wrapping
// its c_import_decl — the namespace name is the unit ref.
for (ns.decls) |nd| {
if (nd.data == .c_import_decl) {
try valid.put(ns.name, {});
break;
}
}
try collectForeignRefTargets(valid, ns.decls);
},
else => {},
}
}
}
fn checkForeignRefs(valid: *const std.StringHashMap(void), decls: []const *Node, diags: *@import("errors.zig").DiagnosticList) void {
for (decls) |d| {
switch (d.data) {
.fn_decl => |fd| {
if (fd.body.data != .foreign_expr) continue;
const ref = fd.body.data.foreign_expr.library_ref orelse continue;
if (!valid.contains(ref)) {
diags.addFmt(.err, d.span, "#foreign library '{s}' is not declared; expected a #library constant or a named '#import c' unit", .{ref});
}
},
.namespace_decl => |ns| checkForeignRefs(valid, ns.decls, diags),
else => {},
}
}
}
/// Validate every `#foreign <ref>` in the merged program: the ref must
/// name a `#library` constant or a NAMED `#import c` unit. Refs are
/// author-side module-local names, but modules merge flat or
/// namespaced into one tree, so existence is checked program-wide.
/// Decls synthesized from `#include` headers carry no ref and are
/// exempt.
pub fn validateForeignRefs(allocator: std.mem.Allocator, root: *const Node, diags: *@import("errors.zig").DiagnosticList) !void {
if (root.data != .root) return;
var valid = std.StringHashMap(void).init(allocator);
defer valid.deinit();
try collectForeignRefTargets(&valid, root.data.root.decls);
checkForeignRefs(&valid, root.data.root.decls, diags);
}
/// A cached entry must at least LOOK like an object file (Mach-O or
/// ELF magic) — a truncated or garbage entry falls back to a fresh
/// compile instead of poisoning the link with an opaque error.