GitNexus/type-resolution-system.md
Gergő Magyar 741a226ab8 feat: Phase 6 type resolution — for-loop Tier 1c, pattern matching, container descriptors, 10-language coverage (#318)
* feat: Phase 6 type resolution — pattern matching, for-loop Tier 1c, coverage completion

- Add patternBindingNodeTypes gate to LanguageTypeConfig for 50% perf improvement
- Expand ForLoopExtractor signature with optional declarationTypeNodes + scope
- Add extractElementTypeFromString shared utility for container type parsing
- Python match/case: extractPatternBinding for `case User() as u:` pattern
- C# refactor: move is_pattern_expression from extractDeclaration to extractPatternBinding
- Ruby: add extractPendingAssignment for assignment chain propagation
- TS/JS: add for-loop Tier 1c for `for (const user of users)` with User[] inference
- Python: add for-loop Tier 1c for `for user in users:` with type annotation inference
- Go: add for-loop Tier 1c for `for _, user := range users` with []User inference
- Fix 'Property' as any stale cast in call-processor.ts
- Add dual return-type string length cap (2048 pre-cap, 512 post-cap)
- Add chain call integration tests for C#, Go, Rust, Python, JS, C++
- Add Python match/case integration test fixtures
- 27 new extractElementTypeFromString unit tests
- 3 for-loop edge cases skipped (declarationTypeNodes scope key lookup)

* fix: address code review findings for Phase 6

- Add missing patternBindingNodeTypes to C# typeConfig (perf gate)
- Add 2048-char input length guard to extractElementTypeFromString
- Skip Python match/case integration tests (call extraction needs query updates)

* reorganise

* fix: Phase 1 bug fixes — Go range semantics, typed_parameter, bracket depth

- Go single-var range correctly returns early for slices/maps (index, not element)
- Go single-var range on channels correctly resolves element type
- Added map_type and channel_type to extractGoElementTypeFromTypeNode
- Added isChannelType helper for channel detection before skip decision
- Added 'typed_parameter' to TYPED_PARAMETER_TYPES for Python annotated params
- Fixed bracket depth tracking in extractElementTypeFromString — only match
  selected closeChar at depth 0, return undefined for mismatched brackets
- Un-skipped 3 prematurely skipped tests (TS local const, Python List/Sequence)
- Added tests for map range, single-var range semantics, bracket edge cases

* refactor: Phase 2 architecture — shared helper, required params, decoupled type nodes

- Extract resolveIterableElementType shared helper in shared.ts implementing
  3-strategy fallback (declarationTypeNodes → scopeEnv string → AST walk)
- Refactor TS, Python, Go extractors to use shared helper (eliminates 3x duplication)
- Make ForLoopExtractor params required (aligned with PatternBindingExtractor)
- Update Java, Kotlin, C# extractor signatures to accept required params
- Decouple declarationTypeNodes from scopeEnv — capture raw type annotation
  nodes BEFORE extractDeclaration for container types (User[], []User, List[User])
- Hybrid approach: direct name extraction + keysBefore fallback for multi-declarator
- Document declarationTypeNodes invariant change (superset of scopeEnv)

* feat: Phase 3 partial — Rust for-loop + C# var foreach Tier 1c

- Rust: add extractForLoopBinding with for_expression support
  - Handles &users, &mut users via reference_expression unwrapping
  - extractRustElementTypeFromTypeNode: generic_type, reference_type, slice/array
  - findRustParamElementType: AST walk with reference/mut pattern unwrapping
  - 4 unit tests (Vec<User>, &[User], range expr negative, no-annotation negative)

- C#: upgrade foreach to handle var (implicit_type) via Tier 1c
  - extractCSharpElementTypeFromTypeNode: generic_name, array_type, nullable_type
  - findCSharpParamElementType: AST walk to method_declaration parameters
  - 3 unit tests (var foreach, explicit type regression, no-annotation negative)

* feat: Phase 3 complete — all language gaps + pattern matching

Kotlin Tier 1c:
- Unannotated for-loop resolves via shared helper
- extractKotlinElementTypeFromTypeNode handles type_projection unwrapping
- findKotlinParamElementType walks to function_declaration

Java Tier 1c:
- var foreach resolves via shared helper
- extractJavaElementTypeFromTypeNode handles generic_type, array_type
- findJavaParamElementType walks to method_declaration

TypeScript:
- readonly User[] unwrapped via readonly_type → array_type recursion

C# switch patterns:
- declaration_pattern added to patternBindingNodeTypes
- extractPatternBinding handles standalone declaration_pattern (switch case/expr)

Rust match arms:
- match_arm added to patternBindingNodeTypes
- extractPatternBinding extended with match_arm → match_expression parent traversal

Python:
- as_pattern tries childForFieldName('alias') before positional fallback

Tests: 237 pass (was 224), 13 new tests added

* feat: Phase 4 — known limitation tests, match arm fix, final verification

- Fix Rust match_arm pattern extraction: unwrap match_pattern to get
  tuple_struct_pattern inside (tree-sitter-rust wraps in match_pattern node)
- Add first-writer-wins regression test for match arm scope leakage
- Add 5 documented skip tests for known limitations:
  - TS destructured for-of (tuple destructuring)
  - Python tuple unpacking in for-loops
  - TS instanceof narrowing (block-level scoping)
  - Rust for with .iter() (method call iterable)
  - Ruby block parameters (closure param inference)

Final: 238 passed, 5 skipped (documented limitations), tsc clean

* test: integration tests for all Phase 6 language gaps + fix Rust param pattern field

Integration test fixtures and tests (30 new tests, all with exact match + negative):

Rust for-loop (5 tests):
- for user in &users with Vec<User> → User#save, negative Repo#save
- for repo in &repos with Vec<Repo> → Repo#save, negative User#save

Rust match arm (5 tests):
- match opt { Some(user) => user.save() } → User#save, negative Repo#save
- if let Ok(repo) = res → Repo#save, negative User#save

C# var foreach (5 tests):
- foreach (var user in users) with List<User> → User#Save, negative Repo#Save
- foreach (var repo in repos) with List<Repo> → Repo#Save

C# switch pattern (4 tests):
- is User user → User#Save, case Repo repo → Repo#Save

Kotlin unannotated for (4 tests):
- for (user in users) with List<User> → user.save, negative repo.save

Go map range (3 tests):
- for _, user := range userMap with map[string]User → User#Save, negative

TypeScript readonly (4 tests):
- for (const user of users) with readonly User[] → user.save, negative

Bug fix: type-env.ts parameter branch now falls back to childForFieldName('pattern')
for Rust parameters (Rust uses 'pattern' not 'name' for parameter names)

* test: add assertion bodies to known limitation skip tests

Convert empty skip test stubs to proper tests with parse/buildTypeEnv/expect
assertions following the codebase convention (e.g., call-processor.test.ts:319).
Each skip test now documents the exact expected behavior, so removing .skip
will cause a meaningful failure when the limitation is eventually fixed.

Also clarify Python integration skip tests as call-extraction issues (not
type-env) and Swift integration skips as build-dep issues (self/super
resolution code already exists in type-env.ts).

* feat: resolve 4 known limitation skip tests + method-aware type arg selection

Unskip 4 of 5 type-env known limitations with full integration test coverage:

1. TS destructured for-of: handle array_pattern by binding last named child
   to element type. Fix Map<K,V> to return last generic arg (value type).
2. Python dict.items() loop: handle `call` iterables + `pattern_list` left
   side. Fix dict[K,V] extraction via type_parameter with last-arg heuristic.
   Unwrap `type` wrapper in extractPyElementTypeFromAnnotation.
3. TS instanceof narrowing: add extractPatternBinding for binary_expression
   with positional child access. First-writer-wins (not block-scoped).
4. Rust .iter() for-loops: handle call_expression in for_expression value
   node by extracting receiver from field_expression.

Method-aware type arg resolution:
- Add TypeArgPosition ('first'|'last') to resolveIterableElementType
- .keys()/.keySet()/.Keys → first type arg (key); all else → last (value)
- Thread position through all 3 strategy callbacks in TS/Rust/Python
- Add predefined_type to extractSimpleTypeName for TS primitives (string etc)

New fixtures: rust-iter-for-loop, typescript-destructured-for-of,
typescript-instanceof-narrowing, python-dict-items-loop.
248 unit tests pass (6 new), 1 skip (Ruby block params).

* feat: container descriptor table for generic type arg resolution

Replace simple KEY_METHODS heuristic with CONTAINER_DESCRIPTORS table
that maps 30+ container types across all languages to their type parameter
semantics per access method.

Key improvements:
- Container-aware resolution: HashMap.iter() correctly yields V (arity 2),
  while Vec.iter() yields T (arity 1) — same method, different semantics
- Cross-language coverage: Map/HashMap/BTreeMap/dict/Dict/Dictionary/
  ConcurrentHashMap + List/Vec/Set/HashSet/Queue/Deque/Stack etc.
- Method categorization: keyMethods (keys/keySet/Keys) vs valueMethods
  (values/get/pop/iter/first/last) per container type
- Fallback for unknown containers: still uses method name heuristic,
  so MyCache<K,V>.keys() correctly returns first arg
- Exported getContainerDescriptor() for future heritage-chain lookups

Each language extractor now passes containerTypeName from scopeEnv to
methodToTypeArgPosition for descriptor-aware resolution.

252 unit tests pass (4 new descriptor tests), 1 skip (Ruby).

* feat: method-aware for-loop extractors + integration tests for all languages

Upgrade 4 existing extractors + create 3 new ones for full cross-language
coverage of call_expression iterables and container descriptor resolution:

Upgraded (add call expr iterable + methodToTypeArgPosition):
- Java: method_invocation (data.keySet(), data.values())
- Kotlin: navigation_expression + call_expression (data.keys, data.values())
- C#: member_access_expression + invocation_expression (data.Keys, data.Values)
- Go: TypeArgPosition threading for Go 1.18+ generics

New for-loop extractors:
- C++: for_range_loop with auto& unwrapping, template_type + qualified_identifier
  (std::vector<User>) extraction, explicit vs auto type handling
- PHP: foreach_statement with simple/key-value/by-reference forms, PHPDoc
  @param priority over AST array type
- Ruby: for-in with YARD @param type resolution via comment parsing

Integration test fixtures + tests for all 6 languages:
- java-map-keys-values (Map.values() + List iteration)
- kotlin-map-keys-values (HashMap.values + List iteration)
- csharp-dictionary-keys-values (Dictionary.Values foreach)
- cpp-range-for (auto& + const auto& range-based for)
- php-foreach-loop (foreach with PHPDoc @param User[])
- ruby-for-in-loop (for-in with YARD @param Array<User>)

Bugs fixed during integration testing:
- C++: qualified_identifier (std::vector) not unwrapped to template_type
- PHP: extractParameter overwrote PHPDoc-derived types with bare 'array'

252 unit tests pass, 201 integration tests pass across 6 languages.

* fix: update extractElementTypeFromString tests for last-arg default

TypeArgPosition change (default 'last') broke 5 existing tests expecting
first arg from multi-arg generics. Updated expectations and added explicit
pos='first' tests for key type extraction.

* fix: rename C++ fixture files to correct case for case-sensitive CI

On case-sensitive filesystems (Linux/macOS CI), git tracked both the old
lowercase files (app.cpp, user.h) and the new uppercase files (App.cpp,
User.h) as separate files. The pipeline processed both, causing the old
app.cpp (with explicit User& type) to interfere with the new auto& test.

Removes old lowercase entries and re-adds with uppercase casing to match
the #include directives in the fixture.

* feat: PR #318 review findings — pattern bindings, member access iterables, structured bindings

Address all 7 genuine gaps identified in PR #318 deep code review:

- Kotlin: add extractKotlinPatternBinding for when/is (type_test AST node)
  with allowPatternBindingOverwrite for smart-cast semantics
- Java: add type_pattern branch for Java 17+ switch pattern variables
- TypeScript: explicit object_pattern skip in for-of (no false bindings)
- Cross-language: member access iterables (self.users, this.users, repo.users)
  across all 10 language extractors
- C++: structured_binding_declarator handling in range-for (last-child heuristic)
- Rust: closure_parameter added to TYPED_PARAMETER_TYPES
- PHP: normalizePhpType handles angle-bracket generics (Collection<User>)

Code review fixes applied:
- Remove 4 debug console.log statements (c-cpp.ts, call-processor.ts)
- Hoist KNOWN_CONTAINER_PROPS to module scope (csharp.ts)
- Guard keysBefore allocation behind typeNode check (type-env.ts)
- Add depth limits (50) to 7 recursive type extraction functions
- Add 2048-char length cap to extractSimpleTypeName
- Fix PHP/Ruby missing typeArgPos parameter in resolveIterableElementType

Integration test fixtures: kotlin-when-pattern, java-switch-pattern,
cpp-structured-binding, typescript-member-access-for-loop,
python-member-access-for-loop

* fix: position-indexed when/is bindings, Kotlin param extraction, HashMap.values for-loop

Three root causes for failing Kotlin integration tests:

1. When/is multi-arm resolution: flat scopeEnv stored only the last arm's
   type (last-writer-wins). Added PatternOverrides with AST range indexing
   so each when arm resolves to its narrowed type independently.

2. HashMap.values for-loop: navigation_expression without call_suffix was
   classified as bare property access (iterableName='values' instead of
   'data'). Now tries object-as-iterable + property-as-method first, with
   fallback to property-as-iterable for this.users patterns.

3. Kotlin parameter extraction: tree-sitter-kotlin parameter nodes use
   positional children (simple_identifier, user_type) not named fields
   (name, type). Added fallback to findChildByType in both
   extractKotlinParameter and extractTypeBinding.

Integration tests added for .keys/.values/Set/MutableMap iteration,
3-arm when/is, multi-call within arms, and when+else branch.

* feat: enhance PHP type resolution for generics and member access in foreach loops

* feat: Phase 6.1 type resolution gap closure — container descriptors, recursive_pattern, class fields

Add 13 missing container type descriptors (Collection, MutableMap, Stream, SortedSet, etc.)
to CONTAINER_DESCRIPTORS for correct element type extraction across C#, Kotlin, and Java.

Extend C# pattern binding to handle recursive_pattern (obj is User { Name: "Alice" } u)
in both is-expression and switch expression contexts.

Add TypeScript class field declaration support (public_field_definition) so for-loop
iteration over this.fieldName resolves element types from class field type annotations.
Includes file-scope fallback in resolveIterableElementType and nested member_expression
handling for this.field.method() patterns.

* docs: add type resolution system documentation with roadmap

Covers the full architecture, resolution tiers (0-2), scope model,
language feature matrix, container descriptors, pipeline integration,
and the Phase 7-9 roadmap for cross-scope propagation, field-type
resolution, and return-type-aware binding.

* feat: Phase 6.2 review findings — C# nested member foreach, C++ deref range-for, Java field_access

Close two gaps found during fourth-pass review of PR #318:

- C# foreach (var user in this.data.Values): nested member_access_expression
  now extracts intermediate property name for scopeEnv lookup
- C++ for (auto& user : *ptr): pointer_expression dereference now recognized
  as range-for iterable

Root causes fixed in shared infrastructure:
- extractSimpleTypeName: add template_type (C++) and generic_name (C#)
- extractGenericTypeArgs: add generic_name for consistency
- type-env.ts: unwrap variable_declaration wrapper in field_declaration
  for declarationTypeNodes capture (zero-allocation manual loop)

Additional review findings addressed:
- Java: add field_access handler for this.data.values() in method_invocation
- C++ pointer_expression: document limitation (*identifier only)
- TypeScript: fix stale comment about property_identifier

All 525 tests pass (278 unit + 247 integration).

* perf: optimize type resolution pipeline — worker threshold, skip graph phases, AST pruning

- Skip worker pool creation for small repos (<15 files or <512KB) — saves 100-400ms
- Add skipGraphPhases option to runPipelineFromRepo to skip MRO/community/process phases
- Add conservative SKIP_SUBTREE_TYPES for leaf-only AST nodes (string, comment, number)
- Pre-compute interestingNodeTypes set — single Set.has() replaces 3 checks per node
- Add fastStripNullable — skip full stripNullable for simple identifiers (90%+ case)
- Replace .children?.find() with manual for loops in extractFunctionName (no array alloc)
- Add hookTimeout: 120000 to vitest.config.ts for CI beforeAll hooks

* fix: review findings — remove template_string from SKIP_SUBTREE_TYPES, handle bare nullable keywords

- Remove template_string and concatenated_string from SKIP_SUBTREE_TYPES
  (template literals contain interpolated expressions with typed code)
- Add FAST_NULLABLE_KEYWORDS check to fastStripNullable for behavioral
  parity with stripNullable on bare null/undefined/void/None/nil
- Add explanatory comment on extractPendingAssignment scopeEnv guard

* feat: add type resolution system and roadmap documentation
2026-03-17 17:10:22 +00:00

15 KiB

Type Resolution System

GitNexus's type resolution system maps variables to likely declared types across the supported languages so the ingestion pipeline can perform receiver-constrained call resolution.

When the code contains a call such as user.save(), the resolver tries to determine that user is a User, allowing call resolution to prefer User#save over unrelated methods such as Repo#save.

This system is designed to be:

  • Conservative — it prefers missing a binding over introducing a misleading one
  • Single-pass — bindings are collected during a single AST walk, with a limited post-pass for assignment propagation
  • Scope-aware — function-local bindings are isolated from file-level bindings
  • Per-file — the environment is built for one file at a time, though it may consult the global SymbolTable for validation in specific cases

It is not a full compiler type checker. Its job is to recover enough type information to improve call-edge accuracy during ingestion.


Purpose in the Pipeline

Type resolution sits between parsing and call resolution.

parse-worker.ts
     │
     ▼
buildTypeEnv(tree, language, symbolTable?)
     │
     ├──► TypeEnvironment.lookup(varName, callNode)
     │         │
     │         ▼
     │    call-processor.ts
     │    - resolves receiver type for method calls
     │    - filters candidates by receiver match
     │    - verifies deferred constructor / initializer bindings
     │
     └──► discarded after file processing

The TypeEnvironment is built once per file. call-processor.ts then uses lookup() to determine receiver types and narrow candidate symbols from the SymbolTable.


Architecture

                                 ┌──────────────────────┐
                                 │     type-env.ts      │
                                 │                      │
                                 │  buildTypeEnv()      │
                                 │  - Single AST walk   │
                                 │  - Scope tracking    │
                                 │  - Tier orchestration│
                                 └──────────┬───────────┘
                                            │ dispatches to
                    ┌───────────────────────┬┴┬────────────────────────┐
                    │                       │ │                        │
          ┌─────────▼──────────┐  ┌─────────▼─▼─────────┐  ┌──────────▼─────────┐
          │   shared.ts        │  │  <language>.ts      │  │    types.ts        │
          │                    │  │                      │  │                    │
          │  Container table   │  │  Per-language        │  │  Extractor         │
          │  Type helpers      │  │  extractors          │  │  interface defs    │
          │  Generic helpers   │  │  (shared + per-lang) │  │                    │
          └────────────────────┘  └──────────────────────┘  └────────────────────┘

Main files

File Purpose
type-env.ts Core engine. Walks the AST once, tracks scopes, collects bindings, and exposes buildTypeEnv() plus the TypeEnvironment interface.
types.ts TypeScript interfaces for extractor hooks such as TypeBindingExtractor, ForLoopExtractor, and PatternBindingExtractor.
shared.ts Language-agnostic helpers such as extractSimpleTypeName, extractElementTypeFromString, resolveIterableElementType, CONTAINER_DESCRIPTORS, and TYPED_PARAMETER_TYPES.
index.ts Dispatch map from SupportedLanguages to LanguageTypeConfig.
typescript.ts TypeScript and JavaScript extractors, including JSDoc support.
jvm.ts Java and Kotlin extractors.
csharp.ts C# extractors.
go.ts Go extractors, including range semantics.
rust.ts Rust extractors, including if let, match-related handling, and Self resolution.
python.ts Python extractors, including match / case handling.
php.ts PHP extractors, including PHPDoc support.
ruby.ts Ruby extractors, including YARD support.
swift.ts Swift extractors. Currently the most minimal configuration.
c-cpp.ts Shared C / C++ extractors.

Supported Languages

The current type-resolution layer supports 13 languages:

  • TypeScript
  • JavaScript
  • Python
  • Java
  • Kotlin
  • C#
  • Go
  • Rust
  • PHP
  • Ruby
  • Swift
  • C
  • C++

Not all languages have the same level of coverage. Swift remains the most minimal. C and some C++ cases naturally benefit less from receiver typing than object-oriented languages.


Design Constraints

The type resolution layer is intentionally narrower than a compiler-grade type system.

It does:

  • resolve variable types from declarations, parameters, initializers, loops, and selected pattern constructs
  • normalize common wrappers such as nullable types and generic containers
  • improve receiver matching during call resolution
  • verify some ambiguous initializer bindings against the SymbolTable

It does not:

  • perform full semantic type checking
  • run fixpoint inference
  • propagate inferred bindings across files as ordinary environment entries
  • model deep field/property chains such as user.address.city
  • guarantee resolution for every ambiguous construct

TypeEnvironment Model

buildTypeEnv() returns a TypeEnvironment that contains:

  • scoped bindings collected from the current file
  • deferred constructor / initializer binding candidates
  • lookup helpers used by call resolution
  • pattern override data for branch-local narrowing where supported

Scope model

The environment is scope-aware so identical variable names in different functions do not collide.

File scope ('')
├── config → Config
├── users → Map
│
├── processUsers@100
│   ├── user → User
│   └── alias → User
│
└── processRepos@200
    └── repo → Repo

Scope keys

  • '' for file scope
  • functionName@startIndex for function-local scope

These scope keys are also used later when verifying deferred bindings in call processing, so any future change to scope-key format must stay consistent across both layers.


Lookup Semantics

TypeEnvironment.lookup() resolves types in this effective order:

  1. special receivers
    • this, self, $this → enclosing class
    • super, base, parent → parent class
  2. position-indexed pattern overrides
  3. function-local scope
  4. file-level scope

Special receivers are handled as a dedicated fast path rather than ordinary lexical bindings.


Resolution Tiers

Bindings are collected during the same AST walk. Higher-confidence sources win over weaker inference.

Tier 0: Explicit Type Annotations

Direct extraction from AST type nodes.

// TypeScript
const user: User = getUser()

// Java
User user = getUser()

// Go
var user User

// Rust
let user: User = get_user()

// Python
user: User = get_user()

extractDeclaration() reads the declaration type node and normalizes it through extractSimpleTypeName().

Parameters are handled separately by extractParameter() using the same normalization logic. The shared TYPED_PARAMETER_TYPES set controls which AST node types are treated as typed parameters.

Tier 0b: For-Loop Element Type Resolution

Also referred to as Tier 1c in Phase 6 PR and test naming.

For-each style loops often introduce a variable with no explicit type. In those cases, the resolver derives the loop variable type from the iterable's container type.

foreach (var user in users) { user.Save(); }

// TypeScript
for (const user of users) { user.save(); }

// Rust
for user in users { user.save(); }

This is handled by resolveIterableElementType() through a three-step cascade:

  1. Declaration type nodes
    Uses raw type annotation nodes when available, including cases such as User[] or List[User].

  2. Scope environment string
    Uses extractElementTypeFromString() to parse a stored type string.

  3. AST walk fallback
    Walks upward to enclosing declarations or parameters when needed.

Tier 0c: Pattern Binding

Pattern-matching constructs may introduce a new variable or temporarily narrow an existing one.

if (obj is User user) { user.Save(); }

// Java
if (obj instanceof User user) { user.save(); }

// Rust
if let Some(user) = opt { user.save(); }

// Python
match obj:
    case User() as user:
        user.save()

Binding behavior depends on the language:

  • first-writer-wins is used by default
  • position-indexed branch overrides are used where branch-local narrowing must not leak between branches, most notably Kotlin

Tier 1: Initializer / Constructor Inference

When there is no explicit annotation, the resolver can infer a type from the initializer.

const user = new User()

// C#
var user = new User()

// Kotlin
val user = User()

// Go
user := User{}
ptr := &User{}
user2 := new(User)

// Ruby
user = User.new

Some languages can identify constructor-like syntax directly. Others need validation through the SymbolTable, because syntax alone cannot always distinguish User() from getUser().

In those cases the system records an unverified binding candidate and later validates it against known class / struct symbols.

Tier 2: Assignment Chain Propagation

Bindings can propagate through simple identifier assignments.

const user: User = getUser()
const alias = user
const other = alias

This is handled after the main walk through a single pass over pending assignments.

This supports simple forward propagation, but there is no iterative fixpoint step. For example:

const b = a
const a: User = getUser()

will not resolve b.


Container Type Descriptors

CONTAINER_DESCRIPTORS defines the type-parameter semantics for common containers.

That allows the resolver to distinguish key-yielding methods from value-yielding methods instead of always assuming the last generic argument.

for (const key of map.keys()) { ... }    // key → string
for (const val of map.values()) { ... }  // val → User

Unknown containers fall back to heuristics, keeping the system conservative rather than fully semantic.

Examples of descriptor-driven behavior

  • Map<K, V> / Dictionary<K, V> / similar key-value containers
  • List<T> / Array<T> / Vec<T> / Set<T> / similar single-element containers
  • method-aware yield selection such as .keys(), .values(), .keySet(), .Values

Comment-Based Types

For less strictly typed ecosystems, the resolver can fall back to documentation-based type information.

Supported comment systems:

  • JSDoc for JavaScript / TypeScript
  • PHPDoc for PHP
  • YARD for Ruby

These are used conservatively and only when AST-level type information is missing or insufficient.


SymbolTable Interaction

Although the environment is built per file, it may consult the global SymbolTable in specific validation paths.

This is important for languages where constructor-like syntax is ambiguous. A binding candidate such as val user = User() may need confirmation that User is a class-like symbol rather than an ordinary function.

This means the system is still per-file in binding construction, but not completely isolated from project-wide symbol knowledge.


Deferred Binding Verification in Call Processing

A key detail is that some initializer bindings are not fully resolved inside TypeEnv itself.

call-processor.ts later verifies deferred bindings and may infer receiver types from:

  • validated class / struct constructor candidates
  • uniquely resolved function or method calls that expose a usable return type

So return-type-aware receiver inference already exists in a constrained downstream form today. What does not yet exist is feeding that information back into TypeEnv broadly enough to power loop inference, general assignment propagation, and wider expression typing.


Language Feature Matrix

Feature TS/JS Java Kotlin C# Go Rust Python PHP Ruby Swift C/C++
Declarations Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes
Parameters Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes
Initializer / constructor inference Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes
Constructor binding scan Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes
For-loop element types Yes Yes Yes Yes Yes Yes Yes Yes Yes No Yes
Pattern binding Yes Yes Yes Yes No Yes Yes No No No No
Assignment chains Yes Yes Yes Yes Yes Yes Yes Yes Yes No Yes
Comment-based types JSDoc No No No No No No PHPDoc YARD No No
Return type extraction JSDoc No No No No No No PHPDoc YARD No No

Current Strengths

The current system already provides strong value for call resolution because it combines:

  • explicit annotation extraction
  • generic-aware loop element typing
  • initializer-based inference
  • selected pattern-based narrowing
  • scope-aware lookups
  • comment-based fallbacks for dynamic ecosystems
  • constrained return-type-aware receiver inference in call processing

This is enough to materially improve call-edge precision even without implementing a full static type system.


Current Limitations

Important gaps still remain:

  • no field / property type map for deep chains such as user.address.city
  • no general cross-file propagation of inferred bindings
  • no fixpoint inference
  • limited branch-sensitive narrowing outside selected pattern constructs
  • limited Swift support compared with other languages
  • no complete destructuring-based field typing
  • no broad expression-level return-type propagation inside TypeEnv

Contributor Notes

When modifying this system, treat the following as load-bearing invariants:

  1. Conservatism matters more than recall
    A missed binding is usually safer than a misleading receiver type.

  2. Scope-key format is shared behavior
    If scope keys change, constructor-binding verification and any downstream lookup using those keys must change in sync.

  3. Tier naming may differ across code and PR discussions
    For-loop element inference may appear as "Tier 0b" in documentation and "Tier 1c" in Phase 6 PR / test naming.

  4. Comment-based types are fallback signals, not primary truth
    They should remain lower-trust than explicit AST-derived types.

  5. Return-type-aware inference already exists in constrained form
    Future roadmap work should extend and generalize it rather than reintroduce it from scratch.