Symbol Resolution in Practice#
After the symbol tables are built, populated with definitions, and linked across modules, the compiler uses them to resolve symbol references throughout the code. This document provides practical examples of how symbol resolution works in various scenarios.
The Symbol Resolution Process#
Symbol resolution in Jaclang involves several key steps:
flowchart TD
A[Symbol Reference] --> B[Current Scope Lookup]
B -->|Found| H[Return Symbol]
B -->|Not Found| C[Inherited Symbol Tables Lookup]
C -->|Found| H
C -->|Not Found| D[Kid Scopes Lookup]
D -->|Found| H
D -->|Not Found| E{Deep Lookup?}
E -->|Yes| F[Parent Scope Lookup]
F --> B
E -->|No| G[Symbol Not Found]
style A fill:#2d3748,stroke:#e2e8f0,stroke-width:1px,color:#e2e8f0
style B fill:#805ad5,stroke:#e2e8f0,stroke-width:1px,color:#e2e8f0
style C fill:#805ad5,stroke:#e2e8f0,stroke-width:1px,color:#e2e8f0
style D fill:#805ad5,stroke:#e2e8f0,stroke-width:1px,color:#e2e8f0
style E fill:#805ad5,stroke:#e2e8f0,stroke-width:1px,color:#e2e8f0
style F fill:#3182ce,stroke:#e2e8f0,stroke-width:1px,color:#e2e8f0
style G fill:#6b7280,stroke:#e2e8f0,stroke-width:1px,color:#e2e8f0
style H fill:#38a169,stroke:#e2e8f0,stroke-width:1px,color:#e2e8f0
The lookup algorithm tries to find symbols in this order:
1. In the current scope's names_in_scope
2. In the inherited_scope
(from imports or inheritance)
3. In the kid_scope
(for entire module imports)
4. In parent scopes (if deep=True
)
Common Resolution Scenarios#
Let's examine how symbol resolution works in various common scenarios.
Local Variable Resolution#
Resolution steps for message
in print(message)
:
1. Look up message
in the greeting
function's scope
2. Find it in names_in_scope
as a local variable
3. Record this use of message
Resolution steps for name
in "Hello, " + name + "!"
:
1. Look up name
in the greeting
function's scope
2. Find it in names_in_scope
as a parameter
3. Record this use of name
Member Access Resolution#
Resolution steps for self.name
:
1. Look up self
in the greet
method's scope
2. Find it in names_in_scope
as an implicit parameter
3. Get the symbol table associated with self
(the Person
architype's symbol table)
4. Look up name
in the Person
symbol table
5. Find it in names_in_scope
as a "has" variable
6. Record this use of name
Cross-Module Resolution#
Given these files:
utils.jac:
main.jac:
Resolution steps for utils.format_name
:
1. Look up utils
in the main
module's scope
2. Find it in kid_scope
as an imported module (added by SymTabLinkPass
)
3. Look up format_name
in the utils
module's symbol table
4. Find it in names_in_scope
as a function
5. Record this use of format_name
Selective Import Resolution#
Given these files:
math.jac:
can add(a: int, b: int) -> int {
return a + b;
}
can subtract(a: int, b: int) -> int {
return a - b;
}
calculator.jac:
import math with add;
can calculate(x: int, y: int) -> int {
return add(x, y); // Works
// return subtract(x, y); // Would fail - not imported
}
Resolution steps for add
:
1. Look up add
in the calculate
function's scope
2. Not found, continue to module scope
3. Look up add
in the calculator
module's scope
4. Not found in names_in_scope
5. Check inherited_scope
(set up by SymTabLinkPass
)
6. Find add
in the InheritedSymbolTable
that references math
7. Record this use of add
If subtract
were uncommented, resolution would fail at step 6 since it's not in the list of imported symbols.
Nested Scopes Resolution#
can outer() {
x = 10;
can inner() {
y = x + 5; // Accessing outer scope
return y;
}
return inner();
}
Resolution steps for x
in y = x + 5
:
1. Look up x
in the inner
function's scope
2. Not found in names_in_scope
3. Check parent scope (outer
function)
4. Find x
in outer
's names_in_scope
5. Record this use of x
Object-Oriented Method Resolution#
node Vehicle {
has wheels: int = 4;
can drive {
print("Driving a vehicle with " + self.wheels + " wheels");
}
}
node Car:Vehicle {
has brand: str;
can drive {
super.drive();
print("It's a " + self.brand);
}
}
Resolution steps for super.drive()
in Car.drive
:
1. Look up super
in the Car.drive
method's scope
2. Find it in names_in_scope
as an implicit variable
3. Get the symbol table associated with super
(the Vehicle
architype's symbol table)
4. Look up drive
in the Vehicle
symbol table
5. Find it in names_in_scope
as a method
6. Record this use of drive
Resolution steps for self.brand
in Car.drive
:
1. Look up self
in the Car.drive
method's scope
2. Find it in names_in_scope
as an implicit parameter
3. Get the symbol table associated with self
(the Car
architype's symbol table)
4. Look up brand
in the Car
symbol table
5. Find it in names_in_scope
as a "has" variable
6. Record this use of brand
Import with Alias Resolution#
Resolution steps for u.helper()
:
1. Look up u
in the module scope
2. Find it in names_in_scope
as an alias (defined through import)
3. Get the symbol table associated with u
(the utils
module's symbol table)
4. Look up helper
in the utils
symbol table
5. Find it in names_in_scope
as a function
6. Record this use of helper
Complex Resolution Scenarios#
Multi-Level Member Access#
node Address {
has city: str;
}
node Person {
has name: str;
has addr: Address;
can print_city {
print(self.addr.city);
}
}
Resolution steps for self.addr.city
:
1. Look up self
in the print_city
method's scope
2. Find it in names_in_scope
as an implicit parameter
3. Get the symbol table associated with self
(the Person
architype's symbol table)
4. Look up addr
in the Person
symbol table
5. Find it in names_in_scope
as a "has" variable
6. Determine the type of addr
is Address
7. Get the symbol table associated with Address
8. Look up city
in the Address
symbol table
9. Find it in names_in_scope
as a "has" variable
10. Record these uses of addr
and city
Inheritance and Shadow Variables#
node Base {
has x: int = 10;
can method {
print(self.x);
}
}
node Derived:Base {
has x: int = 20; // Shadows Base.x
can test {
self.method(); // Prints 20, not 10
}
}
Resolution steps for self.method()
in test
:
1. Look up self
in the test
method's scope
2. Find it in names_in_scope
as an implicit parameter
3. Get the symbol table associated with self
(the Derived
architype's symbol table)
4. Look up method
in the Derived
symbol table
5. Not found in names_in_scope
6. Inherited from Base
, find it in base class
7. Record this use of method
Resolution steps for self.x
in method
when called from Derived
:
1. Look up self
in the method
method's scope
2. Find it in names_in_scope
as an implicit parameter
3. The actual object is a Derived
instance
4. Get the symbol table associated with Derived
5. Look up x
in the Derived
symbol table
6. Find it in names_in_scope
as a "has" variable
7. Record this use of x
Import Conflicts Resolution#
math.jac:
stats.jac:
main.jac:
import math with calculate as math_calc;
import stats with calculate as stats_calc;
can test(x: int) {
a = math_calc(x);
b = stats_calc(x);
return a + b;
}
Resolution steps for math_calc
:
1. Look up math_calc
in the test
function's scope
2. Not found, continue to module scope
3. Look up math_calc
in the main
module's scope
4. Found in names_in_scope
as an alias for math.calculate
5. The symbol points to calculate
in the math
module
6. Record this use of math_calc
Similarly for stats_calc
, but the alias points to calculate
in the stats
module.
Visualization of Symbol Resolution in a Complete Program#
Let's visualize the symbol resolution for a complete program:
import utils with format_string;
node Person {
has name: str;
has age: int;
can describe {
return format_string("Name: {}, Age: {}", [self.name, self.age]);
}
}
can create_person(name: str, age: int) -> Person {
p = Person();
p.name = name;
p.age = age;
return p;
}
can main {
alice = create_person("Alice", 30);
desc = alice.describe();
print(desc);
}
Symbol resolution visualization:
flowchart TD
Main[main.jac Module]
Utils[utils.jac Module]
Main --> Person[Person Architype]
Main --> CreatePerson[create_person Function]
Main --> MainFunc[main Function]
Utils --> FormatString[format_string Function]
Person --> Name[name Has Variable]
Person --> Age[age Has Variable]
Person --> Describe[describe Method]
Describe --> Self[self Parameter]
Self -.-> Person
Describe --> FSRef[format_string Call]
FSRef -.-> FormatString
CreatePerson --> NameParam[name Parameter]
CreatePerson --> AgeParam[age Parameter]
CreatePerson --> P[p Local Variable]
P -.-> Person
P --> PName[p.name Assignment]
PName -.-> Name
P --> PAge[p.age Assignment]
PAge -.-> Age
MainFunc --> Alice[alice Local Variable]
Alice -.-> Person
MainFunc --> CPRef[create_person Call]
CPRef -.-> CreatePerson
MainFunc --> Desc[desc Local Variable]
Alice --> AliceDescribe[alice.describe Call]
AliceDescribe -.-> Describe
Main -.->|Imports| FormatString
style Main fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
style Utils fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
style Person fill:#ffe0b2,stroke:#e65100,stroke-width:2px
style CreatePerson fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
style MainFunc fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
style FormatString fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
style Describe fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
Dotted lines represent symbol resolution links, showing how each reference is resolved to its definition.
Conclusion#
Symbol resolution is a complex but essential part of the compilation process in Jaclang. Understanding how it works in different contexts helps when:
- Debugging name resolution issues
- Implementing new language features
- Understanding how imports and inheritance interact
- Optimizing compiler performance
The combination of well-structured symbol tables and a thorough lookup algorithm ensures that symbols are correctly resolved throughout the program, enabling accurate type checking and code generation.