# How SWC's Resolver Pass Handles Identifier Scope Resolution and Hygiene Tracking

> Discover how SWC's resolver pass uses SyntaxContext and lexical scopes for accurate identifier resolution and hygienic variable renaming across scopes.

- Repository: [swc/swc](https://github.com/swc-project/swc)
- Tags: internals
- Published: 2026-06-15

---

**SWC's resolver pass is a VisitMut transformation that annotates every identifier with a unique SyntaxContext (a chain of Marks) by building lexical scopes, enabling hygienic renaming of variables across different scopes.**

The resolver is the foundation of SWC's hygienic macro system and transpilation pipeline. Implemented in [`crates/swc_ecma_transforms_base/src/resolver/mod.rs`](https://github.com/swc-project/swc/blob/main/crates/swc_ecma_transforms_base/src/resolver/mod.rs), this pass walks the AST to establish lexical scope boundaries and ensure that identically named variables in different scopes receive distinct unique identifiers. Understanding how the resolver handles **identifier scope resolution and hygiene tracking** is essential for anyone building custom transformations or debugging name collisions in the swc-project/swc ecosystem.

## The Core Mechanism: SyntaxContext and Marks

At the heart of SWC's hygiene system is the **`SyntaxContext`**, a lightweight identifier that accompanies every `Ident` node. The resolver pass populates this field by creating a hierarchy of **`Mark`** values—unique integers that represent specific lexical scopes.

When the resolver encounters a binding (variable declaration, function parameter, etc.), it applies the current scope's mark to that identifier's `SyntaxContext`. When it encounters a reference, it searches upward through parent scopes to find the matching declaration, then applies that scope's mark to the reference. The result is that `(Atom, SyntaxContext)` pairs become globally unique identifiers, allowing downstream passes to distinguish between `var a` in an inner block versus `var a` at the top level.

## Scope Construction and Hierarchy

The resolver builds a tree of **`Scope`** structures to track lexical boundaries. Each scope knows its parent, its kind (function, block, module, etc.), and the symbol bindings it contains.

### Scope Structure and Kinds

According to the source in [`crates/swc_ecma_transforms_base/src/resolver/mod.rs`](https://github.com/swc-project/swc/blob/main/crates/swc_ecma_transforms_base/src/resolver/mod.rs), the `Scope` struct (lines 61-76) contains:

- A parent pointer to the enclosing scope
- `ScopeKind` indicating whether this is a function, block, or other scope type
- The `Mark` associated with this specific scope
- Hash maps for declared symbols and, in TypeScript mode, declared types

The resolver begins with a root function-scope carrying the user-provided `top_level_mark`, created in the `Resolver::new` constructor inside the `resolver` function (lines 44-46).

### Entering Child Scopes with `with_child`

When the visitor enters a nested scope (like a block statement or function body), it invokes the **`with_child`** method (lines 36-55). This method:

1. Generates a fresh mark using `Mark::fresh`
2. Creates a new `Scope` with the current scope as its parent
3. Executes the visitor callback within this new scope context
4. Restores the previous scope upon completion

This ensures that bindings inside a nested block do not leak into the parent scope unless they are subject to JavaScript's hoisting rules.

## Recording Bindings and Resolving References

The resolver distinguishes between binding identifiers (declarations) and referencing identifiers (usage). This distinction drives the hygiene annotation process.

### Declaration Handling with `modify`

Every binding identifier passes through the **`modify`** function (lines 33-52). This method:

- Inserts the symbol name into the current scope's declaration hash map
- Applies the current scope's mark to the identifier's syntax context via `id.ctxt = id.ctxt.apply_mark(mark)` (lines 53-56)

This application creates the link between the lexical scope and the AST node. When the same name appears in a different scope later, it receives a different mark, ensuring the pair `(name, context)` remains unique.

### Reference Resolution via `mark_for_ref`

When visiting an identifier in reference position, **`visit_mut_ident`** (lines 54-98) delegates to **`mark_for_ref`** to determine which scope's mark should apply. The algorithm in **`mark_for_ref_inner`** (lines 73-99) performs the following:

1. Checks type-only scopes first if TypeScript mode is enabled
2. Walks up the parent scope chain searching for a matching symbol
3. Returns the found scope's mark, or falls back to `unresolved_mark` for global references
4. Special-cases built-ins like `undefined`, `NaN`, and `Infinity` at the top level, mapping them to `unresolved_mark`

Once the mark is determined, `visit_mut_ident` applies it to the reference identifier's context (lines 72-78), creating the binding-reference link.

### Hoisting with the `Hoister` Sub-Pass

Before the main resolution walk, the resolver executes a **hoisting** sub-pass using the **`Hoister`** struct (lines 78-85). This pre-scan records function declarations and `var`-style variable declarations into their respective scopes before processing actual statements.

In **`visit_mut_module_items`** (lines 68-87), the hoister visits function declarations first, ensuring that later references can resolve to these hoisted bindings even if they appear textually before the declaration in the source code.

## TypeScript Support and Type-Only Scopes

When the `typescript` parameter is `true`, the resolver activates additional logic to prevent type identifiers from leaking into runtime code. The resolver tracks **`declared_types`** in a separate hash map within each scope.

During resolution, **`mark_for_ref_inner`** checks the `handle_types` flag to determine whether to search type-only scopes. The **`visit_mut_import_named_specifier`** method (lines 10-17) distinguishes between value imports and type imports, ensuring that `import type` statements receive appropriate scope treatment without polluting the runtime namespace.

## Practical Implementation Example

To use the resolver in a transformation pipeline, you must provide two distinct marks: `unresolved_mark` for global references and `top_level_mark` for module-level bindings.

Here is a complete example demonstrating the resolver pass:

```rust
use swc_ecma_parser::{Parser, StringInput, Syntax};
use swc_ecma_transforms_base::resolver;
use swc_ecma_visit::FoldWith;
use swc_common::{Mark, FileName, GLOBAL};

fn main() {
    // Parse some JavaScript
    let src = r#"
        let a = 1;
        {
            let a = 2;
            console.log(a);
        }
        console.log(a);
    "#;
    let cm = GLOBAL.clone();
    let fm = cm.new_source_file(FileName::Custom("example.js".into()), src.into());

    let mut parser = Parser::new(
        Syntax::Es(Default::default()),
        StringInput::from(&*fm),
        None,
    );
    let mut module = parser.parse_module().expect("parse error");

    // Create fresh marks for the pass
    let unresolved_mark = Mark::new();
    let top_level_mark   = Mark::new();

    // Apply the resolver (this also adds hygiene information)
    module = module.fold_with(&mut resolver(unresolved_mark, top_level_mark, false));

    // The identifiers now carry proper SyntaxContext marks.
    println!("{:#?}", module);
}

```

After this pass, you can inspect the resolved context of any identifier:

```rust
use swc_ecma_ast::Ident;

fn print_ident_context(ident: &Ident) {
    // The `ctxt` field is a SyntaxContext that stores the Mark chain.
    println!("{} → context = {:?}", ident.sym, ident.ctxt);
}

```

For complete hygiene processing, chain the resolver with the hygiene pass:

```rust
use swc_ecma_transforms_base::{resolver, hygiene};

let mut program = program
    .fold_with(&mut resolver(unresolved_mark, top_level_mark, false))
    .fold_with(&mut hygiene(unresolved_mark));

```

The `hygiene` pass consumes the marks generated by the resolver, removing `unresolved_mark` from bound identifiers and ensuring that only truly global references retain the unresolved context.

## Summary

- The **resolver pass** in [`crates/swc_ecma_transforms_base/src/resolver/mod.rs`](https://github.com/swc-project/swc/blob/main/crates/swc_ecma_transforms_base/src/resolver/mod.rs) implements **identifier scope resolution and hygiene tracking** by annotating every `Ident` with a unique `SyntaxContext`.
- It constructs a hierarchy of **`Scope`** objects linked by parent pointers, using **`Mark::fresh`** to generate unique scope identifiers.
- The **`modify`** function records bindings by applying the current scope's mark to declaration identifiers.
- The **`mark_for_ref`** algorithm walks the scope chain to resolve references, applying the found scope's mark or falling back to **`unresolved_mark`** for globals.
- A **hoisting** pre-pass ensures function and `var` declarations are recorded before references are processed.
- TypeScript support distinguishes value and type bindings via separate hash maps in each scope.
- Downstream passes like **hygiene** rely on these marks to perform safe renaming without name collisions.

## Frequently Asked Questions

### What is a SyntaxContext in SWC?

A **SyntaxContext** is a data structure attached to every identifier in SWC's AST that represents the lexical scope an identifier belongs to. It is implemented as a chain of **Marks**—unique integers generated by `Mark::fresh`. When the resolver pass runs, it populates this field so that two variables with the same name but different scopes receive different contexts, enabling the compiler to distinguish them during transformation.

### How does the resolver handle variable hoisting?

The resolver handles hoisting through a dedicated **`Hoister`** struct that runs as a sub-pass before the main resolution walk. As implemented in `visit_mut_module_items`, this pre-scan visits function declarations and `var` statements first, recording them in their appropriate scopes. This ensures that references appearing before declarations in the source code can still resolve correctly to their hoisted bindings.

### What is the difference between top_level_mark and unresolved_mark?

The **`top_level_mark`** identifies bindings introduced at the module or script's top level (such as imports and top-level declarations), while the **`unresolved_mark`** flags identifiers that could not be resolved to any local binding—typically global variables like `console` or user-defined globals. The `resolver` function accepts both marks as parameters (lines 30-34 of [`mod.rs`](https://github.com/swc-project/swc/blob/main/mod.rs)) and applies them according to the resolution results, allowing downstream hygiene passes to differentiate between module-local and global names.

### Why is the resolver pass necessary before hygiene transformations?

The resolver pass is necessary because it establishes the **canonical identity** of every identifier by assigning it a unique `SyntaxContext`. Without this pass, the hygiene transformation cannot determine which occurrences of a variable name refer to the same lexical binding versus different ones. By generating and attaching these marks, the resolver enables the hygiene pass to safely rename variables and eliminate name collisions without changing program semantics.