Skip to content

Commit a3b309d

Browse files
Add comprehensive documentation on TypeScript binder and symbols system
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
1 parent ad54bfd commit a3b309d

7 files changed

Lines changed: 676 additions & 75 deletions

.github/copilot-library.md

Lines changed: 267 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -91,75 +91,271 @@ new Base(); // Should error - cannot instantiate abstract class
9191
```
9292

9393

94-
# Fourslash Testing
95-
96-
> Fourslash is our testing system for language service functionality
97-
98-
Fourslash tests are interactive TypeScript language service tests. They validate IDE features like completions, quick info, navigation, and refactoring. You create a new fourslash test by putting a file in `tests/cases/fourslash`.
99-
100-
They have a "user code" section, prefixed by four slashes per line, followed by one or more instructions for what to do with the code. Within the code, a `/**/` comment creates an anonymous "marker"; named markers use alphanumeric text between the stars (`/*here*/`). You can use the markers to refer to specific positions in the code:
101-
```typescript
102-
////function foo(x: number) {
103-
//// return x + 1;
104-
////}
105-
////let result = foo(/**/42);
106-
107-
goTo.marker();
108-
verify.baselineSignatureHelp();
109-
```
110-
111-
Use `// @Filename:` to define multiple files:
112-
```typescript
113-
// @Filename: /a.ts
114-
////export const value = 42;
115-
116-
// @Filename: /b.ts
117-
////import { value } from './a';
118-
////console.log(/*marker*/value);
119-
```
120-
121-
Use `[|text|]` to define text ranges, which can be used for selecting text or describing expected Find All References results.
122-
```typescript
123-
////function test() {
124-
//// [|return 42;|]
125-
////}
126-
```
127-
128-
More code examples:
129-
```typescript
130-
// Moving the virtual caret around
131-
goTo.marker("markerName"); // Navigate to marker
132-
goTo.marker(); // Navigate to anonymous marker /**/
133-
134-
// Verifying expected results (generally preferred over baselines in these tests)
135-
verify.currentLineContentIs("expected content");
136-
verify.completions({ includes: "itemName" });
137-
verify.completions({ excludes: "itemName" });
138-
verify.quickInfoIs("expected info");
139-
verify.codeFix({
140-
description: "Fix description",
141-
newFileContent: "expected content after fix"
142-
});
143-
144-
// Completions testing
145-
verify.completions({
146-
marker: "1",
147-
includes: { name: "foo", source: "/a", hasAction: true },
148-
isNewIdentifierLocation: true,
149-
preferences: { includeCompletionsForModuleExports: true }
150-
});
151-
152-
// Code fixes testing
153-
verify.codeFix({
154-
description: "Add missing property",
155-
index: 0,
156-
newFileContent: `class C {
157-
property: string;
158-
method() { this.property = "value"; }
159-
}`
160-
});
161-
162-
// Formatting
163-
format.document();
164-
verify.currentLineContentIs("formatted content");
94+
# TypeScript Binder and Symbols System
95+
96+
> Technical overview of how symbols in TypeScript work
97+
98+
TypeScript's binder creates symbols that represent declarations in your code, and the checker uses these symbols for type checking and name resolution. Understanding this system is crucial for debugging complex issues and understanding how TypeScript resolves names and types.
99+
100+
## What are Symbols?
101+
102+
Symbols are TypeScript's internal representation of declarations. Each symbol has:
103+
104+
- **flags**: A bitmask from `SymbolFlags` enum that describes what kind of declaration it represents
105+
- **escapedName**: The internal string representation of the symbol's name
106+
- **declarations**: Array of AST nodes that declare this symbol
107+
- **members**: SymbolTable for nested symbols (class members, interface properties, etc.)
108+
- **exports**: SymbolTable for module exports
109+
- Internal tracking fields for type checking and resolution
110+
111+
```ts
112+
// From types.ts
113+
export interface Symbol {
114+
flags: SymbolFlags; // Symbol flags
115+
escapedName: __String; // Name of symbol
116+
declarations?: Declaration[]; // Declarations associated with this symbol
117+
valueDeclaration?: Declaration; // First value declaration of the symbol
118+
members?: SymbolTable; // Class, interface or object literal instance members
119+
exports?: SymbolTable; // Module exports
120+
// ... additional internal fields
121+
}
122+
```
123+
124+
## SymbolFlags
125+
126+
SymbolFlags is a bitmask enum that categorizes what kinds of declarations a symbol represents:
127+
128+
```ts
129+
export const enum SymbolFlags {
130+
FunctionScopedVariable = 1 << 0, // var or parameter
131+
BlockScopedVariable = 1 << 1, // let or const
132+
Property = 1 << 2, // Property or enum member
133+
Function = 1 << 4, // Function
134+
Class = 1 << 5, // Class
135+
Interface = 1 << 6, // Interface
136+
TypeAlias = 1 << 19, // Type alias
137+
// ... many more
138+
139+
// Composite flags for common combinations
140+
Variable = FunctionScopedVariable | BlockScopedVariable,
141+
Value = Variable | Property | Function | Class | /* ... */,
142+
Type = Class | Interface | TypeAlias | /* ... */,
143+
}
144+
```
145+
146+
Key composite flags:
147+
- **Value**: Symbols that exist at runtime (variables, functions, classes, etc.)
148+
- **Type**: Symbols that exist only at compile time (interfaces, type aliases, etc.)
149+
- **Namespace**: Symbols that can contain other symbols (modules, enums, etc.)
150+
151+
## SymbolTable
152+
153+
A SymbolTable is simply a `Map<__String, Symbol>` that stores symbols by their escaped names:
154+
155+
```ts
156+
export type SymbolTable = Map<__String, Symbol>;
157+
158+
export function createSymbolTable(symbols?: readonly Symbol[]): SymbolTable {
159+
const result = new Map<__String, Symbol>();
160+
if (symbols) {
161+
for (const symbol of symbols) {
162+
result.set(symbol.escapedName, symbol);
163+
}
164+
}
165+
return result;
166+
}
167+
```
168+
169+
## How the Binder Works
170+
171+
The binder (binder.ts) traverses the AST and creates symbols for declarations. Key functions:
172+
173+
### createSymbol
174+
Creates a new symbol with specified flags and name:
175+
```ts
176+
function createSymbol(flags: SymbolFlags, name: __String): Symbol {
177+
symbolCount++;
178+
return new Symbol(flags, name);
179+
}
180+
```
181+
182+
### declareSymbol
183+
Adds symbols to symbol tables, handling conflicts and merging:
184+
```ts
185+
function declareSymbol(
186+
symbolTable: SymbolTable,
187+
parent: Symbol | undefined,
188+
node: Declaration,
189+
includes: SymbolFlags,
190+
excludes: SymbolFlags
191+
): Symbol
192+
```
193+
194+
The `excludes` parameter defines what kinds of symbols cannot coexist. For example:
195+
- `BlockScopedVariable` excludes all `Value` symbols (no redeclaration)
196+
- `FunctionScopedVariable` excludes `Value & ~FunctionScopedVariable` (can merge with other vars)
197+
- `Interface` excludes `Type & ~(Interface | Class)` (can merge with other interfaces and classes)
198+
199+
### Symbol Resolution Process
200+
201+
During binding, each declaration gets processed:
202+
1. Determine the symbol flags based on declaration type
203+
2. Get or create symbol in appropriate symbol table
204+
3. Check for conflicts using excludes flags
205+
4. Add declaration to symbol's declarations array
206+
5. Set up members/exports SymbolTables if needed
207+
208+
## How Name Resolution Works
209+
210+
The `resolveName` function (created by `createNameResolver` in utilities.ts) implements lexical scoping by walking up the scope chain:
211+
212+
```ts
213+
function resolveNameHelper(
214+
location: Node | undefined,
215+
name: __String,
216+
meaning: SymbolFlags, // What kind of symbol we're looking for
217+
// ...
218+
): Symbol | undefined
219+
```
220+
221+
### Resolution Algorithm
222+
223+
1. **Local Scope Check**: If current node can have locals, check its symbol table
224+
2. **Scope-Specific Rules**: Apply visibility rules based on context:
225+
- Function parameters only visible in function body
226+
- Type parameters visible in parameter list and return type
227+
- Block-scoped variables respect block boundaries
228+
3. **Parent Scope**: Move up to parent node and repeat
229+
4. **Module Exports**: Check module exports if in module context
230+
5. **Global Scope**: Finally check global symbols
231+
232+
### Context-Sensitive Resolution
233+
234+
The `meaning` parameter filters which symbols are considered:
235+
```ts
236+
// Looking for a type
237+
resolveName(location, "x", SymbolFlags.Type, ...)
238+
// Looking for a value
239+
resolveName(location, "x", SymbolFlags.Value, ...)
240+
```
241+
242+
## The Classic Example Explained
243+
244+
```ts
245+
type x = number; // Creates symbol: flags=TypeAlias, name="x"
246+
function fn(x: string) { // Creates symbol: flags=FunctionScopedVariable, name="x"
247+
let y: x = x; // Two different lookups happen here
248+
}
249+
```
250+
251+
When the checker processes `let y: x = x;`:
252+
253+
1. **Type position `x`**:
254+
- Calls `resolveName(location, "x", SymbolFlags.Type, ...)`
255+
- Walks up scopes looking for Type symbols
256+
- Finds the type alias `x = number` in global scope
257+
- Returns that symbol
258+
259+
2. **Value position `x`**:
260+
- Calls `resolveName(location, "x", SymbolFlags.Value, ...)`
261+
- Checks function locals first
262+
- Finds parameter `x: string`
263+
- Returns that symbol
264+
265+
This demonstrates how:
266+
- The same name can resolve to different symbols
267+
- Context (Type vs Value) affects resolution
268+
- Scope hierarchy determines which symbol is found
269+
- The binder creates appropriate symbol tables for different scopes
270+
271+
## Symbol Merging
272+
273+
Some declarations can merge their symbols:
274+
- Multiple `var` declarations with same name
275+
- `interface` declarations merge their members
276+
- `namespace` and `enum` can merge with compatible declarations
277+
- Classes and interfaces can merge (declaration merging)
278+
279+
The binder handles this by checking `excludes` flags and either merging with existing symbols or creating conflicts.
280+
281+
## Debugging Tips
282+
283+
When debugging symbol-related issues:
284+
1. Check what SymbolFlags a symbol has using `symbol.flags & SymbolFlags.SomeFlag`
285+
2. Print symbol names with `symbolToString()` or `symbol.escapedName`
286+
3. Examine symbol.declarations to see all AST nodes for that symbol
287+
4. Use checker's `getSymbolAtLocation()` to see what symbol a node resolves to
288+
5. Check if you're looking for the right meaning (Type vs Value vs Namespace)
289+
290+
# Fourslash Testing
291+
292+
> Fourslash is our testing system for language service functionality
293+
294+
Fourslash tests are interactive TypeScript language service tests. They validate IDE features like completions, quick info, navigation, and refactoring. You create a new fourslash test by putting a file in `tests/cases/fourslash`.
295+
296+
They have a "user code" section, prefixed by four slashes per line, followed by one or more instructions for what to do with the code. Within the code, a `/**/` comment creates an anonymous "marker"; named markers use alphanumeric text between the stars (`/*here*/`). You can use the markers to refer to specific positions in the code:
297+
```typescript
298+
////function foo(x: number) {
299+
//// return x + 1;
300+
////}
301+
////let result = foo(/**/42);
302+
303+
goTo.marker();
304+
verify.baselineSignatureHelp();
305+
```
306+
307+
Use `// @Filename:` to define multiple files:
308+
```typescript
309+
// @Filename: /a.ts
310+
////export const value = 42;
311+
312+
// @Filename: /b.ts
313+
////import { value } from './a';
314+
////console.log(/*marker*/value);
315+
```
316+
317+
Use `[|text|]` to define text ranges, which can be used for selecting text or describing expected Find All References results.
318+
```typescript
319+
////function test() {
320+
//// [|return 42;|]
321+
////}
322+
```
323+
324+
More code examples:
325+
```typescript
326+
// Moving the virtual caret around
327+
goTo.marker("markerName"); // Navigate to marker
328+
goTo.marker(); // Navigate to anonymous marker /**/
329+
330+
// Verifying expected results (generally preferred over baselines in these tests)
331+
verify.currentLineContentIs("expected content");
332+
verify.completions({ includes: "itemName" });
333+
verify.completions({ excludes: "itemName" });
334+
verify.quickInfoIs("expected info");
335+
verify.codeFix({
336+
description: "Fix description",
337+
newFileContent: "expected content after fix"
338+
});
339+
340+
// Completions testing
341+
verify.completions({
342+
marker: "1",
343+
includes: { name: "foo", source: "/a", hasAction: true },
344+
isNewIdentifierLocation: true,
345+
preferences: { includeCompletionsForModuleExports: true }
346+
});
347+
348+
// Code fixes testing
349+
verify.codeFix({
350+
description: "Add missing property",
351+
index: 0,
352+
newFileContent: `class C {
353+
property: string;
354+
method() { this.property = "value"; }
355+
}`
356+
});
357+
358+
// Formatting
359+
format.document();
360+
verify.currentLineContentIs("formatted content");
165361
```

.github/copilot-questions.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
Questions I have that I think the developers of this project can help me with:
2-
* How does control flow analysis represent a circular graph? I checked the documentation server for "cfa" and "control flow"
3-
* How do I tell if a symbol is in the global scope? I checked the documentation server for topics referencing "symbol" and "global"
4-
* What is an `EscapedName`, exactly?
1+
Questions I have that I think the developers of this project can help me with:
2+
* How does control flow analysis represent a circular graph? I checked the documentation server for "cfa" and "control flow"
3+
* How do I tell if a symbol is in the global scope? I checked the documentation server for topics referencing "symbol" and "global"
4+
* What is an `EscapedName`, exactly?
5+
* How does the binder handle complex symbol merging scenarios like when a namespace merges with a class that merges with an interface? I searched for "symbol merging", "declaration merging", and "namespace class interface merge" but want to understand the exact algorithm.

0 commit comments

Comments
 (0)