Skip to content

bug: module system breaks with transitive imports, diamond dependencies, and directory imports in multi-directory projects #1596

@SchoolyB

Description

@SchoolyB

Description

The module system has fundamental bugs that make multi-directory project layouts unusable. These were found through comprehensive stress testing using a realistic project structure (EZDB) with main/, src/, src/engine/, src/models/, src/util/, src/http/, pkg/ directories.

The bugs fall into three categories: path resolution, namespace/naming, and deduplication.


Path Resolution Bugs

Bug 1: Transitive import paths resolve relative to the entry file, not the importing file

When file A imports file B, and B has import "../models/user.ez", that relative path is resolved from A's directory instead of B's directory.

Example layout:

project/
├── main/
│   └── app.ez              ← entry point
└── src/
    ├── models/
    │   └── user.ez
    └── http/
        └── router.ez       ← has: import "../models/user.ez"

Running ez main/app.ez where app.ez imports ../src/http/router.ez:

  • Expected: router.ez's import "../models/user.ez" resolves to src/models/user.ez
  • Actual: Resolves to main/../models/user.ezmodels/user.ez (not found)

This breaks any file that imports relative to its own location when it's pulled in transitively from a different directory.

Bug 2: Cross-module dependencies from within a directory import fail

When a file inside a directory module imports something outside that directory, the path resolves from the entry file's location instead of the file's own location.

src/engine/input.ez:    import "../util/validate.ez"   ← should resolve to src/util/validate.ez

When the entry file is tests/test.ez and imports ../src/engine/, the transitive import in input.ez resolves to tests/../util/validate.ez which doesn't exist.


Namespace & Naming Bugs

Bug 3: Directory import module name resolves to empty string

Importing a directory produces W1002: module '' is imported but never used. The module name should be derived from the directory name (e.g., engine for import "../src/engine/").

Bug 4: Directory-imported functions get underscore prefix instead of directory namespace

When importing ../src/engine/, functions from files within that directory are prefixed with _ instead of engine_. For example, create_game() from core.ez becomes _create_game instead of engine_create_game. This means engine.create_game() resolves to void — the namespace is completely broken.

Visible in warnings:

warning[W1003]: function '_create_game' is declared but never called
warning[W1003]: function '_default_config' is declared but never called
warning[W1003]: function '_zero_vec' is declared but never called

Bug 5: Intra-module struct name mangling is corrupted

When a file within a directory module imports a sibling file (e.g., audio.ez imports ./core.ez), struct names get mangled with the file prefix. Game becomes core_Game, and then field access on core_Game fails:

error[E3010]: struct 'core_Game' has no field 'title'

The struct should either be accessible as Game within the same module or the field lookup should work with the mangled name.

Bug 6: Multiple directory imports collide on empty module name

Importing two different directories in the same file:

import "../src/engine/"
import "../src/models/"

Both resolve to module name "", so the second import triggers:

error[E6001]: module name '' is already imported

Each directory should get its own namespace derived from its directory name.

Bug 7: Name collision across files in same directory uses wrong prefix

When two files in the same directory define a function with the same name, the error reports the wrong name:

error[E4004]: function '_init' already declared

The function should be collision_init (directory-namespaced), not _init.


Deduplication Bugs

Bug 8: Diamond dependencies cause false E6001 errors

When the entry file imports module X directly, and also imports module Y which transitively imports the same module X, the compiler treats the transitive copy as a duplicate:

import "../src/util/validate.ez"
import "../src/engine/"          // engine/input.ez also imports validate.ez

Produces:

error[E6001]: module name 'validate' is already imported

The compiler should recognize both paths resolve to the same file and deduplicate silently.

Bug 9: Directory import + file import of same content collides

import "../src/models/"              // pulls in user.ez and session.ez
import "../src/models/user.ez"       // also imports user.ez directly

Produces E6001: module name 'user' is already imported. The compiler should deduplicate by resolved absolute path.

Bug 10: Self-referential directory import not handled

A file inside engine/ that imports ../engine/ (its own directory) doesn't infinite loop (good), but also doesn't produce a clear error. It should either be silently ignored (the file is already part of the module) or produce a specific error like "cannot import own module directory".


Expected Behavior

Once fixed, the module system should support this workflow:

project/
├── main/
│   └── main.ez                     ← entry point
├── src/
│   ├── engine/
│   │   ├── core.ez                 ← defines Game struct, create_game(), start()
│   │   ├── renderer.ez             ← defines RenderConfig, default_config()
│   │   ├── physics.ez              ← defines Vec2, zero_vec(), add_vec()
│   │   ├── audio.ez                ← imports ./core.ez, uses Game
│   │   └── input.ez                ← imports ../util/validate.ez
│   ├── models/
│   │   ├── user.ez
│   │   └── session.ez              ← imports ./user.ez
│   └── util/
│       ├── validate.ez
│       └── format.ez               ← imports ../models/user.ez
└── pkg/
    └── shared/
        └── constants.ez
// main/main.ez
import "../src/engine/"              // directory import — all files as one namespace
import "../src/models/user.ez"       // file import
import "../pkg/shared/constants.ez"  // file import from separate tree

do main() {
    mut g = engine.create_game("My Game")    // from engine/core.ez
    mut cfg = engine.default_config()         // from engine/renderer.ez
    mut v = engine.zero_vec()                 // from engine/physics.ez
    mut u = user.new_user("Alice", "[email protected]", 30)
    println(constants.APP_NAME)
}

Key requirements:

  1. Relative imports resolve from the file that contains the import statement
  2. Directory imports register all contents under the directory name as namespace
  3. Files within a directory module can import siblings with ./sibling.ez
  4. Files within a directory module can import outside with ../other/file.ez
  5. Diamond dependencies deduplicate by resolved absolute path
  6. Directory + file import of same content deduplicates silently
  7. Multiple directory imports each get their own namespace
  8. Name collisions across files in the same directory produce clear errors with correct names
  9. Self-referential directory imports are handled gracefully

Affected Code

  • ezc/src/main.c — import resolution, path construction, directory scanning, module name derivation
  • Possibly ezc/src/typechecker/typechecker.c — namespace/prefix registration for directory-imported symbols

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcliCommand-line interface relatedcriticalMust be fixed immediately

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions