Feat(ck_resolve): N3 local & item scope resolution — LocalId + E0301/E0305 #9

Manually merged
hkowalski merged 5 commits from feat/009-feat-ck-resolve-n3-local-item-scope-resolution into master 2026-01-14 08:00:00 +01:00
Owner

Feat(ck_resolve): N3 local & item scope resolution — LocalId + E0301/E0305

Along the way:

  • feat(ck_resolve): N3 local & item scope resolution — LocalId + E0301/E0305
  • feat(modules): land module loader/linker (L1–L4) + qualified-path resolution (N4)
  • feat(modules): land L5 — module dependency graph + cycle detection (E0601)
  • feat(ck_resolve): land N5 — header/impl matching (E0401/E0403; E0402 retired)
  • Merge branch 'master' into feat/009-feat-ck-resolve-n3-local-item-scope-resolution

Touches 54 files: docs/impl/04-name-resolution.md, docs/impl/08-diagnostics.md, docs/impl/module-system/README.md, docs/impl/resolution/N3-local-scope-resolution.md, docs/impl/resolution/N4-qualified-paths.md, docs/impl/resolution/N5-header-impl-matching.md and 48 more.

Feat(ck_resolve): N3 local & item scope resolution — LocalId + E0301/E0305 Along the way: - feat(ck_resolve): N3 local & item scope resolution — LocalId + E0301/E0305 - feat(modules): land module loader/linker (L1–L4) + qualified-path resolution (N4) - feat(modules): land L5 — module dependency graph + cycle detection (E0601) - feat(ck_resolve): land N5 — header/impl matching (E0401/E0403; E0402 retired) - Merge branch 'master' into feat/009-feat-ck-resolve-n3-local-item-scope-resolution Touches 54 files: `docs/impl/04-name-resolution.md`, `docs/impl/08-diagnostics.md`, `docs/impl/module-system/README.md`, `docs/impl/resolution/N3-local-scope-resolution.md`, `docs/impl/resolution/N4-qualified-paths.md`, `docs/impl/resolution/N5-header-impl-matching.md` and 48 more.
hkowalski approved these changes 2026-01-14 07:55:00 +01:00
hkowalski left a comment

A clear improvement. The change in src/rust/crates/ck_resolve/src/graph.rs is the part I would have argued for.

A clear improvement. The change in `src/rust/crates/ck_resolve/src/graph.rs` is the part I would have argued for.
@ -0,0 +66,4 @@
/// Whether the graph holds no modules.
#[must_use]
pub fn is_empty(&self) -> bool {
Owner

is_empty is public. Does anything outside the crate call it, or is it pub because that was easier? Every public item is a promise.

`is_empty` is public. Does anything outside the crate call it, or is it pub because that was easier? Every public item is a promise.
@ -0,0 +353,4 @@
}
#[test]
fn duplicate_module_name_across_two_impls_fires_e0602_naming_both() {
Owner

duplicate_module_name_across_two_impls_fires_e0602_naming_both reads, decides and writes in one function. The deciding part is the piece I would want to test on its own. Worth splitting?

`duplicate_module_name_across_two_impls_fires_e0602_naming_both` reads, decides and writes in one function. The deciding part is the piece I would want to test on its own. Worth splitting?
@ -0,0 +354,4 @@
let (caller_tree, caller_src) = (&trees[last], files[last].1);
let (caller_name, _) = crate::module_name(caller_tree, caller_src, &mut interner);
let caller = linked.modules.module(caller_name).expect("caller module");
let scope = linked.modules.scope(caller).expect("caller scope");
Owner

The expect message on scope is the only thing a user sees if this fires. Does it say what they should do, or only what we assumed?

The expect message on `scope` is the only thing a user sees if this fires. Does it say what they should do, or only what we assumed?
Author
Owner

Nothing outside calls is_empty. It is pub(crate) in src/rust/crates/ck_resolve/src/graph.rs now.

Nothing outside calls `is_empty`. It is pub(crate) in `src/rust/crates/ck_resolve/src/graph.rs` now.
Author
Owner

Split. The decision in duplicate_module_name_across_two_impls_fires_e0602_naming_both is a pure function now, with its own tests in src/rust/crates/ck_resolve/src/link.rs.

Split. The decision in `duplicate_module_name_across_two_impls_fires_e0602_naming_both` is a pure function now, with its own tests in `src/rust/crates/ck_resolve/src/link.rs`.
Author
Owner

It said what I assumed. Reworded in src/rust/crates/ck_resolve/src/paths.rs so it names the input that broke it, which is what the bug report will need.

It said what I assumed. Reworded in `src/rust/crates/ck_resolve/src/paths.rs` so it names the input that broke it, which is what the bug report will need.
Walk every fn body and const initializer, assign a per-body LocalId to each
binder (let / params / pattern / match-arm / lambda / for / catch), and
resolve every value-position reference against a scope-frame stack. Implements
the locked shadowing rule (docs/impl/resolution/N3-local-scope-resolution.md):
lexically-closest-wins with a silent nested shadow; a same-scope duplicate
binder is E0305; an unresolved single-segment reference is E0301. Qualified
paths defer to N4.

All SyntaxKind matching for body walking lives in the new ck_syntax body
projection (bodies.rs: classify_expr/classify_pat + typed wrappers), because
ck_resolve does not depend on ck_lex and so cannot name a SyntaxKind — the
resolver matches only typed Expr/Pat enums. Span gains derived Ord so per-
reference resolutions key a BTreeMap.

N3 landed ahead of the module loader: its block-scope mechanics resolve
against the single-module symbol table N1 produces, and the loader's merged
.ckh/.ck scope later enriches the module-scope fallback without changing them.
Deferred (resolve to nothing today): item scope, stdlib/primitive scope,
qualified paths, cons-pattern heads / bare-variant disambiguation.

- ck_syntax/bodies.rs: Expr/Pat classification + accessors over the CST
- ck_resolve/bodies.rs: resolve_module_bodies + scope stack + 9 unit tests
- ck_cktest: pipeline runs N3 per module after the collect pass
- tests/resolve: 4 goldens (clean body, silent shadow, E0301, E0305);
  6 existing parser/lex fixtures re-blessed (real E0301s for undefined refs)
- docs: register E0301/E0305; mark N3 landed (#32) across the slice index
The module system's front end and cross-module name resolution, on the
n2-effect-uniqueness branch. The doc updates for these slices are
intertwined across the same files, so they land as one commit.

- N4 (#35): value-position qualified paths (`Mod::fn`, `Effect::op`).
  ck_resolve::paths anchors the head to a module or effect, narrows each
  `::` segment, kind-checks the final segment (E0303), enforces structural
  privacy (E0302); an unresolved head/segment is E0301. Adds
  PathExpr::segments() to ck_syntax. Data-variant and type-position paths
  stay deferred.

- L3/L4 (#33/#34): ck_resolve::link — merge a module's header/impl into one
  ModuleScope (collect_file_decls flips has_impl) and assemble the
  project-global ProjectModuleTable; E0602 on a duplicate `mod Name`. Built
  loader-agnostically over the file set, as N2 did.

- L1/L2 (#36/#37): ck_load grows into the on-disk loader. FileSystem gains
  read_dir; DiskFs joins MemFs. load_project walks a real directory (or a
  fixture's MemFs) into the parsed/paired ModuleFile set the linker consumes:
  walk-up root + project.ck CkLang manifest (name + local-path deps,
  recursive closure with a visited-set), src/-or-root discovery (excluding
  project.ck/config*.ck), .ckh/.ck pairing. Diagnostics: E0603 (filename ≠
  mod Name), E0404 (a function-declaring header with no impl), E0604 (dep
  source absent). The loader surfaces only structural diagnostics — parse
  errors stay the parser's surface (a header's bodyless fn signatures are
  expected, since the parser has no header mode yet).

ck_load gains front-end deps (EXPECTED_GRAPH + R1 updated, still acyclic).
Tests: ck_load MemFs unit tests, a DiskFs test loading each tests/corpus app
as its own root (each gains a project.ck), and an end-to-end load→link→resolve
test. Docs (modules.md, module-system/, resolution/, 08-diagnostics, slices)
mark the slices landed and record the resolved manifest + layout decisions.
The linker's second table: after the L4 module table, `link_project` builds
a `ModuleGraph` from the cross-module references in each module's bodies.

- `ck_resolve::graph` — mines a module→module edge from every qualified path
  head that names another module, produces a dependency-first resolution
  order, and detects cycles via a three-colour DFS. A back-edge to a module
  still on the recursion stack fires E0601, naming the full `A → B → A` path
  and pointing at the reference that closes it.
- The head kind-check is extracted as `paths::classify_head` and shared
  verbatim with N4, so the graph sees exactly the cross-module references N4
  resolves and the two passes can never disagree on what a head names.
- `ck_hir` ids gain `Ord`/`PartialOrd` (dense allocation index) so handles
  key the graph's deterministic BTree collections.
- E0601 flows through `LinkedProject.diagnostics`, so the .cktest harness
  surfaces it with no extra wiring; golden `tests/resolve/module_cycle.cktest`.

The resolution order is built and tested but not yet consumed — N4 resolves
by direct table lookup (the full L4 table exists before resolution, so name
resolution is order-independent); the order is for the next order-sensitive
pass (N5 header/impl matching).

Closes the loader/linker cascade (L1–L5). Remaining in the module system:
nested module paths (Net::Tcp) and stdlib unit injection.
Close per-module resolution with the .ckh ↔ .ck cross-check, run inside the
linker once a module's two files are merged. Only functions pair across the
two files (a record/data/const lives wholly in one), so it matches header vs
impl `fn`s by name:

- E0401 — a header `fn` signature with no matching impl body (primary on the
  header declaration).
- E0403 — a header `fn` whose impl signature diverges, as a self-contained
  structural-shape diff (primary on the impl declaration, both shapes in the
  message). Shape = new ck_syntax FnDecl::signature_shape: the significant-
  token text of generics + params + return type + effect clause, body omitted;
  syntactic, not resolved types, so N5 stays a name-resolution check.

E0402 is retired, not implemented: under purely-structural visibility an
impl-only declaration is simply private, never an error in itself (modules.md
enforcement rule 2), so the reserved producer-side "impl-public without header"
check has no trigger. The consumer-side privacy error is E0302 (N4), whose
message now names the remedy — declare it in the owning module's header to make
it public. 08-diagnostics.md gains a "retired row" convention.

Goldens: tests/resolve/{header_without_impl,signature_mismatch,header_impl_match}.cktest;
private_path.cktest re-blessed for the new E0302 message. Docs updated:
N5 spec (status → landed, E0402 decision), resolution README, slices.md (#39),
diagnostics registry. ck_resolve crate docstring refreshed to cover N1–N5.
Brings in the type-system design (TY1–TY8 scaffold + docs) and the
"require lambda annotations" parser change (mandatory `: TypeExpr` on
`LambdaParam`, E0112).

Conflict resolution — docs/impl/slices.md "After the parser" table:
- Module loader/linker + Name resolution rows: kept this branch's (L1–L5
  and N1–N5 all landed; master's rows were stale "in progress").
- Type system row: took master's (TY1–TY8 scaffolded, unblocked).

Global slice-number collision: master assigned #32 to "Lambda parameter
annotations" (its next free number, having forked before this branch's
N3–N5/L3–L5 claimed #32–#39). Reconciled by moving the single isolated
lambda slice to the next free number (#40); the contiguous, pervasively-
referenced resolution/loader block keeps #32–#39. Recorded in slices.md.

Adjustments for the merged grammar:
- ck_resolve N3 test `lambda_for_and_match_binders_resolve`: annotated its
  lambda (`(y) => y` → `(y: Int) => y`) so it feeds valid syntax, not an
  error-recovered tree, post the mandatory-annotation rule.
- ck_parser tests/lambda.rs: applied `cargo fmt` (a master-side wrap miss)
  so the merged tree passes `cargo fmt --check`.

Gates: cargo fmt --check, clippy -D warnings, and cargo test --workspace
all clean.
hkowalski manually merged commit 522e4a1546 into master 2026-01-14 08:00:00 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
northgale/cklang-bootstrap!9
No description provided.