plum

#treesitter#compiler#wasm

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

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


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