Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
98935b8
feat(defs): identify `impl` blocks and their methods
claude Aug 23, 2026
9745fae
refactor(elaborator): key the impl index by `DefId`
claude Aug 23, 2026
5d48d18
refactor(elaborator): key every method and trait signature by `DefId`
claude Aug 23, 2026
43e3c88
fix(elaborator): resolve a bare call from its own reference site
claude Aug 23, 2026
7cce36b
refactor(elaborator): key every free-function signature by `DefId`
claude Aug 23, 2026
a3d661d
refactor(elaborator): name the `impl` block a dispatch was recorded a…
claude Aug 23, 2026
6de4d18
fix(elaborator): name a generic impl's target at its own header site
claude Aug 24, 2026
26e8ffb
fix(elaborator): answer a call from its site before reaching for a name
claude Aug 24, 2026
8c8f7cf
refactor(elaborator): ask the declaration table once per question
claude Aug 24, 2026
ed1b220
docs(compiler): name the phases `elaborate` actually runs
claude Aug 24, 2026
15a6c44
refactor(elaborator): reach the declaration table through the walker
claude Aug 24, 2026
de855ad
docs(wep): record what the impl index carrying `DefId`s changed
claude Aug 24, 2026
063dd9a
refactor(elaborator): ask an impl header once whether it is concrete
claude Aug 24, 2026
3916000
docs(wep): state what closing the local-`impl` gap takes
claude Aug 24, 2026
773dd6f
merge origin/main (conflicts unresolved)
claude Aug 24, 2026
0d2727b
resolve merge conflicts
claude Aug 24, 2026
3d7d6c8
chore: tidy
wado-bot[bot] Aug 24, 2026
2a53f7f
fix(elaborator): key a static call's impl lookup on the receiver's de…
claude Aug 24, 2026
5980bd8
Merge remote-tracking branch 'origin/claude/elaborator-architecture-r…
claude Aug 24, 2026
d798f16
test: golden WIR for the cross-module conversion fixture
claude Aug 24, 2026
b59a14c
merge origin/main (conflicts unresolved)
claude Aug 24, 2026
f142ffd
resolve merge conflicts
claude Aug 24, 2026
70379b7
fix(elaborator): key static-dispatch metadata by the resolved receiver
claude Aug 24, 2026
a62c723
chore: tidy
wado-bot[bot] Aug 24, 2026
d18175d
fix(elaborator): un-alias an impl header's head before matching a rec…
claude Aug 24, 2026
f1233aa
test: golden WIR for the aliased impl head fixture
claude Aug 24, 2026
dcea3bd
fix(elaborator): keep an impl header's binder head unaliased
claude Aug 24, 2026
0870af9
refactor(elaborator): one receiver-key derivation for static lookups
claude Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions docs/compiler.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ The Wado compiler (`wado-compiler/`) translates `.wado` source into a Wasm compo
```
Source (.wado)
→ Lex → Parse → Bind (per module, in loader)
→ Annotate (Analyze + Resolve + lower TIR)
→ Analyze → Resolve
→ Annotate (decls, then bodies) → Liveness → Reify (TIR)
→ Default-purity Check
→ Synthesis (auto-derives, template, From, serde, pre-CM effect dispatch, CM bindings)
→ Effect Check → Stores Check
Expand All @@ -31,7 +32,11 @@ The driver is `compile_after_load` in `src/lib.rs`.
| Lex / Parse | AST | `lexer.rs`, `parser.rs`, `token.rs`, `syntax.rs` |
| Bind | AST + bindings | `bind.rs` |
| Loader | All modules | `loader.rs` |
| Annotate | TIR + facts | `semantics.rs`, `analyze.rs`, `elaborator/` |
| Analyze | `SymbolTable` | `analyze.rs` |
| Resolve | Declarations | `defs.rs`, `resolve.rs` |
| Annotate | `Semantics` | `semantics.rs`, `elaborator/` |
| Liveness | Reachability | `elaborator/liveness.rs` |
| Reify | `TirModule` | `elaborator/reify.rs` |
| Default-purity Check | (validation) | `effect_check.rs::check_default_purity` |
| Synthesis | TIR (extended) | `synthesis/` |
| Effect / Stores | TIR (validated) | `effect_check.rs` |
Expand All @@ -51,7 +56,7 @@ The driver is `compile_after_load` in `src/lib.rs`.
| Unit | Layer |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `Module` (`ast.rs`) | Surface AST. Preserves source-level syntax to support `wado format`. |
| `TirModule` (`tir.rs`) | Typed IR. One per source module after annotate. |
| `TirModule` (`tir.rs`) | Typed IR. One per source module, emitted by reify. |
| `Package` (`package.rs`) | Per-module compilation context, used from synthesis through link. |
| `FlatPackage` (`flat_package.rs`) | Flat list of all functions, types, and globals; used from monomorphize through the lower pipeline's planner. |
| `NirPackage` (`nir_package.rs`) | Normalized IR. Output of lower, input to optimize / WIR build / codegen. |
Expand All @@ -69,9 +74,16 @@ The loader runs `lexer → parser → bind` on every loaded module:

The AST is parser-immutable from this point on. The desugar-replacement surface rewrites — compound assignment (`x += y` → `x = x + y`), `while` / C-style `for` → explicit `loop`, `for x of expr` iteration (the `.into_iter()` / `.next()` dispatch and the `match Some(x) => body, _ => break` shape), the `assert` statement, the `matches` operator, the comparison chain `a < b < c`, template-string interpolations, `use … namespace` prefix stripping (`helper::foo`), and `Self::method` / `T::method` (T bound to concrete) static-call dispatch — happen inside the elaborator and are built TIR-direct: each rewrite resolves the user AST and constructs `TirExpr` / `TirStmt` nodes directly without producing synthetic AST. The implementations live in `elaborator/{stmt,operators,assert,matches}.rs` (`resolve_while`, `resolve_for`, `resolve_iterator_for_of`, `resolve_compound_assign`, `desugar_assert`, `desugar_matches_expr`, `desugar_comparison_chain`), `Elaborator::strip_ns_prefix` in `elaborator.rs`, and `CalleeIdentKind` / `classify_call_callee` in `elaborator/call.rs` (the prefix is resolved to its concrete type name before parameter-type lookup so argument resolution runs once with the correct expected-type hints). Synthetic call sites that need to dispatch a method on an already-resolved receiver TIR (the for-of `.into_iter()` / `.next()` calls today) reuse the AST-driven method dispatch via `Elaborator::resolve_method_call_with` (`elaborator/method_call.rs`) — that helper takes a pre-resolved receiver plus a method name and signals "no source AST" with `method_id: None` so no use→def edge is recorded against the synthesis site. Keeping the AST parser-shaped is what lets LSP queries land on the user's text rather than on a synthesised replacement.

## Annotate (Analyze + Resolve + TIR Lowering)
## Analyze and Elaborate

`semantics_of` (`semantics.rs`) is the entry point shared by LSP and batch compilation. It runs `analyze.rs` for symbol-table construction and `elaborator/` for type checking; bodies are then lowered into TIR.
`semantics_of` (`semantics.rs`) is the entry point shared by LSP and batch compilation. It runs `analyze.rs` for the symbol table, then `elaborator/` for the phases below ([WEP 2026-05-26](./wep-2026-05-26-elaborator-rearchitecture.md)):

- **Resolve** answers every reference site once, from the module that wrote it, and identifies every declaration (`defs.rs`, `resolve.rs`, [WEP 2026-08-12](./wep-2026-08-12-declaration-identity.md)). A reference site is answered nowhere else; the positions no site answers derive their `DefId` from a module the caller names.
- **Annotate** runs a declaration pass over every module — types, `TraitEnv`, `Signatures` — then a body walk whose sole output is a populated `ModuleSemantics`.
- **Liveness** computes source-level reachability, which gates what reify emits and feeds the unused diagnostics.
- **Reify** reads the recorded facts back and emits one `TirModule` per module. No inference, no dispatch decisions.

The LSP path stops after liveness and builds no TIR.

The result, `Semantics`, carries the TIR modules plus an `AstIndex` and a use→def map (`AstId → AstId`, globally-unique ids). This is what makes the architecture LSP-friendly: facts are attached to AST nodes without mutating them, so cross-file navigation, hover, and rename all fall out of the same data the batch compiler uses. See the [LSP](#lsp) section below.

Expand Down
2 changes: 1 addition & 1 deletion docs/wep-2026-05-26-elaborator-rearchitecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ and the surviving suppression is the argument-classification probe.
logger, interner, invocations, entry module) behind one `ElabEnv` field,
dissolve `AnnotateState` — `tysys` and `module_semantics` land on
`Semantics`, the rest are driver locals — and collapse the per-module
construction site. Takes `Elaborator` from 11 fields to 7.
construction site.

## Consequences

Expand Down
30 changes: 27 additions & 3 deletions docs/wep-2026-07-09-local-item-definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,30 @@ whether reify or this WEP's eager annotate-time emission produced it.
generic `type` surfaces as a type mismatch, since it needs a different
mechanism — `GenericNewtypeInfo` + AST substitution, not the monomorphized
template generic structs use — that this WEP does not wire up. Methods on
any local type surface as `no method 'x' found on type`, since a local
`impl`/`trait` block parses but is not connected to method dispatch, a
module-wide registry built once before any function body is walked.
any local type surface as `no method 'x' found on type`.

## Known gap: methods on a local type

Two module-wide registries are built before any function body is walked, and a
local `impl` needs both.

- `TraitEnv` indexes `module.items`, so no local block reaches it. A body walk
can be added: the target's head has its own reference site, and a block whose
target is a function-local declaration is scoped by that identity, since two
sibling blocks' same-named types are two declarations.
- `Signatures` is assembled between the declaration pass and the body walk and
is read-only after. A local `impl`'s target is a block-local type whose
`TypeId` is interned _during_ that walk, so its method signatures cannot
exist before the freeze.

Closing it means either giving a local type declaration its durable identity
and `TypeId` in the declaration pass — `declare_local_struct` needs only the
`DefId`, which `DefTable::build` already mints, and sits in the body walk for
_visibility_ — or making `Signatures` extensible after assembly, which gives up
the read-only rule [`wep-2026-05-26`](./wep-2026-05-26-elaborator-rearchitecture.md)
rests on.

Two things a first attempt hits: the orphan rule reads a function-local target
as foreign, because `type_decl_index` holds module-level declarations only; and
indexing `TraitEnv` alone is _worse_ than the gap, since the header then
resolves where the signature does not and the diagnostic becomes a panic.
13 changes: 10 additions & 3 deletions docs/wep-2026-08-12-declaration-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,9 +467,16 @@ a sited entry point a caller with a reference site reaches instead.
The same derivation in the `Symbol` currency, which §5's `DefId` columns subsume:

- `symbol_named`, `imported`, `lookup_in_module`, `lookup_in_module_with_visited`

One rendering still compared against a declaration's own, which goes when the
impl index carries `DefId`s:
- `decl_in_module` — `lookup_in_module` read back as an identity, for the
positions no reference site answers: `builtin::f`, a namespace member,
`core:rt`'s `panic` at a synthesised call, and a default expression's own
module. Each names its module rather than searching for one, so no vantage
is supplied.

One rendering still compared against a declaration's own, on the paths that
name no block: an auto-derived `Eq` / `Ord`, and a method reached through a
type parameter's bound, whose block monomorphization picks. A dispatch that
matched a concrete block reads the module off it instead.

- `impl_target_decl_key` — a receiver's newtype chain against an impl's head

Expand Down
146 changes: 131 additions & 15 deletions wado-compiler/src/defs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ pub enum DefKind {
VariantCase,
/// A `flags` member.
FlagsMember,
/// A method declared by a trait, a resource, or an effect interface.
/// An `impl` block. It writes no name, so nothing can import it and no
/// scope answers for it; its identity exists so the methods it declares
/// have an owner and its own facts have a key.
Impl,
/// A method declared by a trait, a resource, an effect interface, or an
/// `impl` block.
Method,
}

Expand Down Expand Up @@ -206,20 +211,22 @@ impl DefTable {
// it would otherwise be the one type in the language with no
// declaration to name it.
for item in &ast.items {
if let Item::TupleTypeDecl(decl) = item
&& !table.by_ast_id.contains_key(&decl.id)
{
table.declare(Def {
ast_id: decl.id,
module: module.clone(),
name: crate::name::TUPLE_TYPE_NAME.to_string(),
kind: DefKind::BuiltinType,
visibility: decl.visibility,
span: Some(decl.span),
parent: None,
function_local: false,
members: Vec::new(),
});
match item {
Item::TupleTypeDecl(decl) if !table.by_ast_id.contains_key(&decl.id) => {
table.declare(Def {
ast_id: decl.id,
module: module.clone(),
name: crate::name::TUPLE_TYPE_NAME.to_string(),
kind: DefKind::BuiltinType,
visibility: decl.visibility,
span: Some(decl.span),
parent: None,
function_local: false,
members: Vec::new(),
});
}
Item::Impl(block) => table.declare_impl_block(module, block, false),
_ => {}
}
}
table.declare_members(module, ast);
Expand Down Expand Up @@ -256,12 +263,41 @@ impl DefTable {
}
}

/// Identify an `impl` block, which no symbol table row names. Its name is
/// empty because it writes none, and no spelling reaches what has none.
fn declare_impl_block(
&mut self,
module: &ModuleSource,
block: &crate::ast::ImplBlock,
function_local: bool,
) {
if self.by_ast_id.contains_key(&block.id) {
return;
}
self.declare(Def {
ast_id: block.id,
module: module.clone(),
name: String::new(),
kind: DefKind::Impl,
visibility: Visibility::Private,
span: Some(block.span),
parent: None,
function_local,
members: Vec::new(),
});
}

/// Identify a function-local item (`Stmt::Item`) and its members.
///
/// A local `struct` is a declaration like any other — two functions writing
/// the same spelling declare two of them — so it gets an identity rather
/// than a mangled storage name standing in for one.
fn declare_local_item(&mut self, module: &ModuleSource, item: &Item) {
if let Item::Impl(block) = item {
self.declare_impl_block(module, block, true);
self.declare_item_members(module, item);
return;
}
let (kind, name) = match item {
Item::Struct(d) => (DefKind::Struct, &d.name),
Item::Enum(d) => (DefKind::Enum, &d.name),
Expand Down Expand Up @@ -343,6 +379,15 @@ impl DefTable {
i.methods.iter().map(|m| (m.id, &m.name, None, m.span)),
),
),
Item::Impl(b) => (
b.id,
members(
DefKind::Method,
b.methods
.iter()
.map(|m| (m.id, &m.name, Some(m.visibility), m.span)),
),
),
_ => return,
};
let Some(owner) = self.of_ast_id(owner) else {
Expand Down Expand Up @@ -439,6 +484,14 @@ impl DefTable {
/// nothing.
///
/// This is not a name lookup: the node is already the declaration.
/// [`Self::of_ast_id`] for a node the collect pass identified, so a miss is
/// a hole in that pass rather than a case to handle.
#[must_use]
pub fn def_at(&self, id: AstId) -> DefId {
self.of_ast_id(id)
.unwrap_or_else(|| panic!("{id:?} declares nothing"))
}

#[must_use]
pub fn of_ast_id(&self, id: AstId) -> Option<DefId> {
self.by_ast_id.get(&id).copied()
Expand Down Expand Up @@ -637,6 +690,68 @@ mod tests {
assert_eq!(field_names(widgets[1]), ["b"]);
}

/// An `impl` block is a declaration: two blocks writing the same method
/// name declare two methods, and each block owns its own.
#[test]
fn an_impl_block_and_its_methods_are_declarations() {
let source = r#"
pub struct Point { x: i32 }
pub struct Line { a: i32 }
impl Point { pub fn len(&self) -> i32 { return self.x; } }
impl Line { pub fn len(&self) -> i32 { return self.a; } }
"#;
let (defs, module) = build_from_source(source);
let blocks: Vec<DefId> = defs
.iter()
.filter(|d| defs.kind(*d) == DefKind::Impl)
.collect();
assert_eq!(blocks.len(), 2);
assert_ne!(blocks[0], blocks[1]);
// No spelling reaches an `impl` block, so it renders none.
assert_eq!(defs.name(blocks[0]), "");
assert_eq!(defs.module(blocks[0]), &module);

for block in &blocks {
assert_eq!(defs.members(*block).len(), 1);
let method = defs.members(*block)[0];
assert_eq!(defs.name(method), "len");
assert_eq!(defs.kind(method), DefKind::Method);
assert_eq!(defs.parent(method), Some(*block));
}
assert_ne!(defs.members(blocks[0])[0], defs.members(blocks[1])[0]);
}

/// A local `impl` block is declared by the function that writes it, so its
/// methods are identified like any other block's.
///
/// Identity only: `TraitEnv::build` walks a module's own items, so nothing
/// a local block declares is dispatchable yet. Giving it one is what that
/// walk needs to reach `Stmt::Item`, not something this table withholds.
#[test]
fn a_function_local_impl_block_is_a_declaration_of_its_own() {
let source = r#"
pub fn run() -> i32 {
struct Widget { a: i32 }
impl Widget { fn get(&self) -> i32 { return self.a; } }
let w = Widget { a: 1 };
return w.get();
}
"#;
let (defs, _) = build_from_source(source);
let block = defs
.iter()
.find(|d| defs.kind(*d) == DefKind::Impl)
.expect("the local impl block is a declaration");
assert!(defs.is_function_local(block));
assert_eq!(
defs.members(block)
.iter()
.map(|m| defs.name(*m))
.collect::<Vec<_>>(),
["get"]
);
}

/// A cached declaration fact carries a [`DefId`], so a later compile that
/// re-identifies the same declarations must hand back the same ones — the
/// stdlib snapshot reads its `ImplSig`s back this way.
Expand All @@ -645,6 +760,7 @@ mod tests {
let source = r#"
pub struct Point { x: i32, y: i32 }
pub trait Greet { fn hello(&self) -> i32; }
impl Point { pub fn len(&self) -> i32 { return self.x; } }
"#;
let (seed, modules, symbols, _) = analyze_source(source);
let again = DefTable::build_seeded(Some(&seed), &modules, &symbols);
Expand Down
Loading