plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


docs/superpowers/specs/2026-07-23-cross-file-imports-design.md
6dfb06a 1
# Design: cross-file import resolution
6dfb06a 2
6dfb06a 3
## Problem
6dfb06a 4
6dfb06a 5
Every `libs/std/*.plum` file declares `module std`, but there's no actual
6dfb06a 6
module-resolution mechanism — `ast::Import { path }` is parsed and then
6dfb06a 7
completely discarded; `plum-cli`'s `compile` command only ever reads and
6dfb06a 8
compiles the single file it's given. `libs/std/list.plum` references
6dfb06a 9
`Option`/`Node`; `Node` is declared in the same file, but `Option` lives in
6dfb06a 10
`libs/std/option.plum` — so `list.plum` cannot type-check or compile standalone
6dfb06a 11
today. This is the second (and, after this lands, last) half of README's
6dfb06a 12
"Known gaps" bullet about `libs/std`'s `List`/`Map` not fully compiling.
6dfb06a 13
6dfb06a 14
Wiring up `List`'s remaining `todo` methods (`add`/`set`/`removeAt`/`remove`/
6dfb06a 15
`clear`/`reverse`) is explicitly **out of scope** for this spec — it's the next,
6dfb06a 16
separate cycle. This spec only makes cross-file `import` actually resolve, so
6dfb06a 17
`list.plum` (with its currently-implemented methods; still-`todo` ones keep
6dfb06a 18
trapping at runtime exactly as they do today) compiles standalone for the
6dfb06a 19
first time.
6dfb06a 20
6dfb06a 21
## Scope
6dfb06a 22
6dfb06a 23
In scope:
6dfb06a 24
- `import <path>` resolves `<path>` (a `/`-separated string, e.g. `std/option`)
6dfb06a 25
  to a file on disk and merges that file's declarations into the compiling
6dfb06a 26
  program.
6dfb06a 27
- Transitive imports (A imports B, B imports C: A sees C's declarations too).
6dfb06a 28
- Diamond imports (A imports both B and C, both import D: D's declarations
6dfb06a 29
  appear exactly once, not duplicated).
6dfb06a 30
- Import cycles (A imports B, B imports A) resolve without an infinite loop or
6dfb06a 31
  a spurious error — this is normal, permitted structure, not a mistake.
6dfb06a 32
- A clear, specific error for: an import path that doesn't resolve to any file;
6dfb06a 33
  two merged files declaring the same top-level name.
6dfb06a 34
- Adding the one missing `import std/option` line to `libs/std/list.plum` (the
6dfb06a 35
  concrete fix this whole spec exists to enable) and proving, via a real
6dfb06a 36
  on-disk test, that `list.plum` now type-checks and compiles standalone.
6dfb06a 37
6dfb06a 38
Out of scope:
6dfb06a 39
- `List`'s own `todo` methods (separate, later cycle).
6dfb06a 40
- A package manager, versioning, or any registry — resolution is purely
6dfb06a 41
  path-based against one configurable root directory.
6dfb06a 42
- Selective/aliased imports (`import std/option { Some, None }`, `import ... as
6dfb06a 43
  x`) — the grammar only supports a bare path today: `import <path>` always
6dfb06a 44
  pulls in everything that path's file declares.
6dfb06a 45
- Re-validating or fixing the OTHER `libs/std/*.plum` files' existing `import`
6dfb06a 46
  lines that reference nonexistent targets (e.g. `http.plum`'s `import
6dfb06a 47
  std/path` and `import std/http/content_type`, neither of which corresponds
6dfb06a 48
  to a real file in this repo). Those files are a rough, partial port from
6dfb06a 49
  another language's stdlib pseudo-syntax (per their own `NOTE:` comments) and
6dfb06a 50
  aren't this spec's concern — this spec makes `import` itself work
6dfb06a 51
  correctly; it doesn't audit every existing library file's import list.
6dfb06a 52
- `Module`/`module <name>` gets no new semantics — it stays exactly as
6dfb06a 53
  decorative/unused as it is today. Resolution is driven entirely by
6dfb06a 54
  `Import.path`, never by matching one file's `module` name against another's.
6dfb06a 55
6dfb06a 56
## Path resolution
6dfb06a 57
6dfb06a 58
`import std/option` resolves against a single configurable root directory
6dfb06a 59
(the "lib path"): `<lib-path>/std/option.plum`. `plum-cli`'s `compile`
6dfb06a 60
subcommand gains a `--lib-path <dir>` flag, defaulting to `./libs` (relative to
6dfb06a 61
the current working directory) — matching today's actual `libs/std/*.plum`
6dfb06a 62
layout with no extra configuration needed to build the standard library from
6dfb06a 63
the repo root.
6dfb06a 64
6dfb06a 65
## Architecture
6dfb06a 66
6dfb06a 67
Add a loader ahead of the existing, **unchanged** `plum_checker::check_source`
6dfb06a 68
/ `plum_wasm_codegen::compile_source` — those two functions still only ever
6dfb06a 69
see a single, already-fully-merged `ast::Source` and need no awareness that
6dfb06a 70
multiple files were involved. The loader lives in `plum-core` (alongside
6dfb06a 71
`AstParser`, which it uses directly) as a new small module:
6dfb06a 72
6dfb06a 73
```rust
6dfb06a 74
pub fn load_and_merge(entry: &Path, lib_path: &Path) -> Result<ast::Source, String>
6dfb06a 75
```
6dfb06a 76
6dfb06a 77
Algorithm: parse `entry` into a `Source`. Walk its `imports` (and, recursively,
6dfb06a 78
every newly-loaded file's own `imports`) with a `visited: HashSet<PathBuf>` of
6dfb06a 79
*canonicalized* file paths already parsed. For each import path not yet
6dfb06a 80
visited: resolve it to `<lib_path>/<path>.plum`, error clearly if that file
6dfb06a 81
doesn't exist, otherwise parse it, mark it visited, append its `items` to the
6dfb06a 82
running merged list (recording each item's name -> source file for collision
6dfb06a 83
detection), and recurse into *its* imports before returning to the caller's
6dfb06a 84
remaining imports. An already-visited import is silently skipped (this is what
6dfb06a 85
makes diamond imports and cycles both "just work" — a cycle's second visit to
6dfb06a 86
an already-in-progress file is indistinguishable, at this level, from a diamond
6dfb06a 87
import's second visit to an already-finished one; both hit the `visited` check
6dfb06a 88
and stop).
6dfb06a 89
6dfb06a 90
Name-collision detection: as items are appended, check each item's name (a
6dfb06a 91
function/method key, class name, enum name, or const name — one extraction
6dfb06a 92
function per `Item` variant, mirroring how `plum-checker`'s own
6dfb06a 93
`build_global_tables` already walks `source.items`) against a running
6dfb06a 94
`HashMap<String, PathBuf>` of name -> the file that first declared it; a name
6dfb06a 95
seen again from a *different* file is an error naming both files. (Two
6dfb06a 96
`name<Receiver>(...)` methods with the same method name but different
6dfb06a 97
receivers are NOT a collision — key on `(receiver, name)` for methods exactly
6dfb06a 98
the way `plum-checker`'s `MethodEnv` already does, so unrelated methods that
6dfb06a 99
happen to share a name across files, e.g. two different classes each defining
6dfb06a 100
`length`, don't falsely collide.)
6dfb06a 101
6dfb06a 102
The result: one `ast::Source` whose `items` is the union of every reachable
6dfb06a 103
file's items (each appearing exactly once), which `plum-cli` hands to the
6dfb06a 104
existing `check_source`/`compile_source` unchanged.
6dfb06a 105
6dfb06a 106
## `plum-cli` integration
6dfb06a 107
6dfb06a 108
`cmd_compile` always routes through `load_and_merge` (even a zero-import file
6dfb06a 109
trivially returns just itself) — the special-cased single-file parse it does
6dfb06a 110
today is removed, not kept as a fallback. `Compile` gains a `--lib-path <dir>`
6dfb06a 111
argument (default `"libs"`), passed straight through to `load_and_merge`.
6dfb06a 112
6dfb06a 113
## Testing
6dfb06a 114
6dfb06a 115
- `plum-core` loader unit tests (using `tempfile`-style on-disk fixtures — real
6dfb06a 116
  files in a temp directory, not in-memory strings, since the loader's whole
6dfb06a 117
  job is filesystem resolution):
6dfb06a 118
  - a file importing another file sees the imported file's declarations
6dfb06a 119
  - transitive import (A -> B -> C) surfaces C's declarations to A
6dfb06a 120
  - diamond import (A -> B, A -> C, B -> D, C -> D) includes D's declarations
6dfb06a 121
    exactly once
6dfb06a 122
  - an import cycle (A -> B -> A) resolves without hanging or erroring
6dfb06a 123
  - an import path that resolves to no file is a clear, specific error
6dfb06a 124
  - two merged files declaring the same top-level name is a clear, specific
6dfb06a 125
    error naming both files
6dfb06a 126
  - two different classes/methods across files sharing a method name (but
6dfb06a 127
    different receivers) is NOT an error
2172082 128
- `libs/std/list.plum` gains the one new line: `import std/option` — a
2172082 129
  correct, useful fix on its own regardless of the point below.
2172082 130
- A new end-to-end `plum-cli` integration test (matching the existing
2172082 131
  `plum-cli/tests/compile_tests.rs` convention: invoke the actual built
2172082 132
  `plum` binary via `std::process::Command`, using
2172082 133
  `concat!(env!("CARGO_MANIFEST_DIR"), "/...")`-relative fixture paths) that
2172082 134
  proves the *loader mechanism* end-to-end: two small, purpose-built fixture
2172082 135
  files (one importing the other) compile successfully via `plum compile
2172082 136
  <entry> --lib-path <dir>`.
2172082 137
  **Not** in scope for this test: asserting that the real `libs/std/list.plum`
2172082 138
  fully type-checks/compiles standalone. It doesn't, independent of this
2172082 139
  spec — `list.plum`'s `join` method calls `Buffer()`, and `Buffer` isn't
2172082 140
  declared anywhere in this repo; more generally, `plum-checker` doesn't
2172082 141
  process `Item::Trait` at all yet (no trait-bounded dispatch, e.g. `a.toStr()`
2172082 142
  on a generic constrained to `Stringable`). Both are separate, unscoped gaps.
2172082 143
  This spec only makes the *import mechanism* correct and proven; it doesn't
2172082 144
  make every existing `libs/std` file type-check.
6dfb06a 145
6dfb06a 146
## README
6dfb06a 147
6dfb06a 148
Once this lands, the "Known gaps" bullet's cross-file-import clause is
6dfb06a 149
removed entirely, leaving only the `List`-methods-still-`todo` clause (now
6dfb06a 150
correctly describing it as blocked on nothing but not-yet-done work, since
6dfb06a 151
both of its former prerequisites — variadic parameters and cross-file
6dfb06a 152
imports — are complete).