lang: reject dir-vs-file ambiguous #import

An extensionless import path that names a directory next to a same-named
.sx file ('modules/std' with both modules/std.sx and modules/std/ present)
no longer silently resolves to the directory — it errors and asks for the
explicit .sx spelling. Exemption: a file importing its own companion
directory (X.sx importing X/, the multi-file test layout) stays legal —
the sibling file is the importer itself, so the directory is the only
sensible target.
This commit is contained in:
agra
2026-06-11 08:37:36 +03:00
parent 12bf61a9fc
commit 698f75d79a
7 changed files with 50 additions and 1 deletions

View File

@@ -0,0 +1,11 @@
// An extensionless #import that matches BOTH a `.sx` file and a sibling
// directory of the same name is ambiguous — the compiler refuses to pick
// silently and asks for the explicit `.sx` spelling. `modules/std` is the
// canonical collision: `modules/std.sx` (the prelude) sits next to
// `modules/std/` (mem/fs/process/...).
#import "modules/std";
main :: () -> s32 {
0
}

View File

@@ -0,0 +1,5 @@
error: ambiguous import 'modules/std': both a file 'modules/std.sx' and a directory 'modules/std' exist — write "modules/std.sx" to import the file
--> examples/1158-diagnostics-import-dir-file-ambiguous.sx:7:1
|
7 | #import "modules/std";
| ^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -398,6 +398,10 @@ Direct C header import:
math :: #import "modules/math"; // namespaced import (directory: all .sx files merged) math :: #import "modules/math"; // namespaced import (directory: all .sx files merged)
``` ```
A path that matches both a file and a same-named sibling directory
(`modules/std.sx` next to `modules/std/`) is rejected as ambiguous — write the
`.sx` path to import the file.
When two flat-imported modules each define a function of the same name, every When two flat-imported modules each define a function of the same name, every
module's own code binds its OWN author — a bare call resolves to the same-name module's own code binds its OWN author — a bare call resolves to the same-name
function in the caller's module (or in its single flat import that provides it). function in the caller's module (or in its single flat import that provides it).

View File

@@ -2495,6 +2495,8 @@ pkg :: #import "modules/math"; // namespaced — all .sx files merged under pk
Directory imports scan only the top level of the specified directory (non-recursive). Files are processed in alphabetical order for deterministic builds. Files within the directory may `#import` each other or external files. Directory imports scan only the top level of the specified directory (non-recursive). Files are processed in alphabetical order for deterministic builds. Files within the directory may `#import` each other or external files.
If an extensionless path matches both a file and a sibling directory of the same name (`modules/std.sx` next to `modules/std/`), the import is an error — write the `.sx` path to import the file. Exception: a file importing its own companion directory (`X.sx` importing `X/`) is not ambiguous; the directory is the only sensible target.
Namespaced declarations are accessed with dot notation: Namespaced declarations are accessed with dot notation:
```sx ```sx
std.print("hello"); std.print("hello");

View File

@@ -1074,7 +1074,32 @@ pub fn resolveImports(
try cache.put(resolved_path, result); try cache.put(resolved_path, result);
break :blk result; break :blk result;
} else |_| { } else |_| {
// File read failed — try as directory import // File read failed — try as directory import. An extensionless
// path that names a directory next to a same-named `.sx` file
// is ambiguous: require the explicit `.sx` spelling for the
// file rather than silently picking the directory. Exception:
// when the sibling `.sx` is the importing file itself (a test
// importing its own companion directory), the directory is the
// only sensible target.
const sibling_sx = try std.fmt.allocPrint(allocator, "{s}.sx", .{resolved_path});
const sibling_exists = if (std.mem.eql(u8, sibling_sx, file_path))
false
else if (std.Io.Dir.readFileAlloc(.cwd(), io, sibling_sx, allocator, .limited(10 * 1024 * 1024))) |_|
true
else |_|
false;
if (sibling_exists) {
const is_dir = if (std.Io.Dir.openDir(.cwd(), io, resolved_path, .{})) |d| dir_blk: {
d.close(io);
break :dir_blk true;
} else |_| false;
if (is_dir) {
if (diagnostics) |diags| {
diags.addFmt(.err, decl.span, "ambiguous import '{s}': both a file '{s}.sx' and a directory '{s}' exist — write \"{s}.sx\" to import the file", .{ imp.path, imp.path, imp.path, imp.path });
}
return error.ImportError;
}
}
const result = resolveDirectoryImport(allocator, io, resolved_path, chain, cache, source_map, diagnostics, decl.span, stdlib_paths, import_graph, flat_import_graph, comptime_ctx) catch { const result = resolveDirectoryImport(allocator, io, resolved_path, chain, cache, source_map, diagnostics, decl.span, stdlib_paths, import_graph, flat_import_graph, comptime_ctx) catch {
if (diagnostics) |diags| { if (diagnostics) |diags| {
diags.addFmt(.err, decl.span, "cannot read import '{s}' (not a file or directory)", .{resolved_path}); diags.addFmt(.err, decl.span, "cannot read import '{s}' (not a file or directory)", .{resolved_path});