You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/plans/2026-01-21-dsl-definitions-design.md
+44-41Lines changed: 44 additions & 41 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -49,7 +49,9 @@ struct DslDefinition {
49
49
}
50
50
```
51
51
52
-
The `DslDefinition` is pushed onto the nesting stack when visiting its block, so any definitions inside (methods, nested DSL calls, etc.) become members of the `DslDefinition` rather than the lexical parent.
52
+
The `DslDefinition` is pushed onto the nesting stack (via new `Nesting::Dsl` variant) when visiting its block, so any definitions inside (methods, nested DSL calls, etc.) become members of the `DslDefinition` rather than the lexical parent.
53
+
54
+
A `ConstantReference` is also created for `receiver_name` so it gets resolved naturally in Phase 2.
53
55
54
56
#### ConstantToDslDefinition
55
57
@@ -77,14 +79,14 @@ For non-constant cases (`var = Class.new` or just `Class.new`), only `DslDefinit
77
79
78
80
### Phase 2: Resolution
79
81
80
-
Standard resolution runs, which resolves `receiver_name` to a fully qualified name (e.g., `Class` in scope `Foo` → `::Class`).
82
+
Standard resolution runs, which resolves `receiver_name` to a fully qualified name (e.g., `Class` in scope `Foo` → `::Class`). The `ConstantReference` created for `receiver_name` ensures it gets resolved through the normal reference resolution path.
81
83
82
-
### Phase 3: DSL Processing (New Phase)
84
+
### Phase 3: DSL Processing
83
85
84
-
After resolution, process all `ConstantToDslDefinition`s:
86
+
A new method `process_dsl_definitions()` is called after `resolve_all()`. It processes all `ConstantToDslDefinition`s:
85
87
86
88
1. Look up the referenced `DslDefinition`
87
-
2. Get the resolved receiver name
89
+
2. Get the resolved receiver via `graph.names().get(&receiver_name)`
88
90
3. Dispatch to handler based on resolved receiver + method name:
-**Handler matches (e.g., `::Class.new`)**: Transform `ConstantToDslDefinition` into `DynamicClassDefinition` with the constant name. Creates `ClassDeclaration`.
99
101
-**No handler matches (e.g., `SomeClass.new`)**: Transform `ConstantToDslDefinition` into regular `ConstantDefinition`. We don't know what `SomeClass.new` returns.
100
102
101
-
Each handler also decides what to do with owned members (reassign, discard, etc.).
103
+
Each handler also decides what to do with owned members (reassign to the new definition, discard, etc.). When members are reassigned, their `lexical_nesting_id` is NOT updated - it preserves the lexical structure as written in source code.
102
104
103
105
### Phase 4: Incremental Resolution
104
106
105
-
Run resolution again for any new definitions created in Phase 3. This should be fast since the set is small.
107
+
Call `resolve_all()` again for any new definitions created in Phase 3. The full resolution approach is acceptable since the set of new definitions is small.
108
+
109
+
**Full resolution flow:**
110
+
111
+
```rust
112
+
resolver.resolve_all(); // Phase 2
113
+
resolver.process_dsl_definitions(); // Phase 3
114
+
resolver.resolve_all(); // Phase 4
115
+
```
106
116
107
117
## DslDefinition Details
108
118
@@ -129,7 +139,7 @@ struct DslArgumentList {
129
139
130
140
### Block Ownership
131
141
132
-
All DSL calls automatically own their block contents. This simplifies indexing logic—the handler decides what to do with owned definitions during processing:
142
+
All DSL calls automatically own their block contents. This simplifies indexing logic - the handler decides what to do with owned definitions during processing:
133
143
134
144
- Reassign them to a new owner
135
145
- Discard them
@@ -138,7 +148,7 @@ All DSL calls automatically own their block contents. This simplifies indexing l
138
148
139
149
### Lifecycle
140
150
141
-
`DslDefinition` is kept after processing. It shares a declaration with any generated definitions, enabling tooling features like "hover over `belongs_to` → see generated methods".
151
+
`DslDefinition` is kept after processing. It shares a declaration with generated definitions, enabling tooling features like "hover over `belongs_to` → see generated methods".
- Late assignment of anonymous classes (`klass = Class.new; Foo = klass`)
327
352
-`Struct.new` handler (complex: inherits from Struct, generates accessor methods)
328
353
- Plugin API for external DSL targets
329
354
- RSpec/Minitest support via plugins
@@ -332,29 +357,7 @@ impl DslDefinition {
332
357
333
358
1.**Memory impact**: One-to-many relationship between definitions and declarations. Monitor memory usage during implementation.
334
359
335
-
## Design Decisions Made
336
-
337
-
1.**Constant assignment detection**: Use `ConstantToDslDefinition` to link constants to DSL calls. Similar pattern to `index_constant_alias_target` in existing code. Detected in `visit_constant_write_node` via new `index_dsl_definition_target` helper.
338
-
339
-
2.**Anonymous classes**: `DynamicClassDefinition` uses `name_id: Option<NameId>`. Named classes get `Some(name)` from `ConstantToDslDefinition`, anonymous classes get `None` and no declaration is created.
340
-
341
-
3.**Declaration membership**: For `Bar = Class.new`, `Declaration(Foo::Bar)` contains two definitions: `DslDefinition` and `DynamicClassDefinition`. This enables tooling to trace generated code back to the DSL call.
342
-
343
-
4.**Unknown receiver fallback**: If `ConstantToDslDefinition` references a DSL whose receiver doesn't match any handler (e.g., `Foo = SomeClass.new`), it becomes a regular `ConstantDefinition`.
344
-
345
-
5.**lexical_nesting_id preservation**: When members are reassigned from `DslDefinition` to `DynamicClassDefinition`, their `lexical_nesting_id` is NOT updated. It preserves the lexical structure as written in source code.
346
-
347
-
6.**Phase 4 resolution**: Simply call `resolve_all` again. The set of new definitions is small, so full resolution is acceptable.
348
-
349
-
7.**Edge cases not handled**: Conditional assignments (`Foo = cond ? Class.new : Module.new`), method chaining (`Class.new.freeze`), and late assignment of anonymous classes (`klass = Class.new; Foo = klass`) are out of scope.
350
-
351
-
8.**Phase 3 location**: New method `process_dsl_definitions()` called after `resolve_all()`. Caller does: `resolver.resolve_all(); resolver.process_dsl_definitions(); resolver.resolve_all();`
352
-
353
-
9.**Receiver resolution**: During indexing, create a `ConstantReference` for `DslDefinition.receiver_name`. It gets resolved naturally in Phase 2. Phase 3 looks it up via `graph.names().get(&receiver_name)`.
354
-
355
-
10.**DynamicClassDefinition ID**: Use same location as source `DslDefinition` with DSL name: `format!("{}{}Class.new", uri_id, offset)`. Similarly `Module.new` for `DynamicModuleDefinition`.
0 commit comments