Skip to content

_resolve_base_type_name recurses without cycle guard (post-#630 inconsistency) #633

@aallan

Description

@aallan

Origin: test-analyzer agent finding C1 / silent-failure-hunter M1 / code-reviewer S4 on PR #631.

The pattern

_resolve_base_type_name in vera/wasm/inference.py:~1340 is a recursive alias-chain follower with no cycle guard:

def _resolve_base_type_name(self, name: str) -> str:
    if name not in self._type_aliases:
        return name
    alias = self._type_aliases[name]
    if isinstance(alias, ast.RefinementType):
        if isinstance(alias.base_type, ast.NamedType):
            return self._resolve_base_type_name(alias.base_type.name)
    if isinstance(alias, ast.NamedType):
        return self._resolve_base_type_name(alias.name)
    return name

The deleted helper _resolve_type_name_to_wasm_canonical (removed in #630) had explicit cycle protection via a seen set. Its replacement _canonical_named_type (the new central walker) also has cycle protection. But _resolve_base_type_name continues to recurse without one, and is called from ~9 sites across inference.py plus vera/wasm/data.py and vera/wasm/context.py.

Reachability

Empirically verified during PR #631 review: a cyclic type alias (type A = B; type B = A;) crashes the codegen with RecursionError at vera/codegen/core.py:_type_expr_to_wasm_type before reaching _resolve_base_type_name. So the immediate vulnerability is masked by a different recursive resolver crashing first.

This means: _resolve_base_type_name is currently dead-code-safe with respect to cycles. But:

  • If the type checker ever lets a cycle through and the upstream _type_expr_to_wasm_type is fixed first, this becomes the next crash site.
  • The inconsistency itself is a code-smell — the new walker has cycle protection, the old helper doesn't, and they sit side by side.

Recommended fix

Either:

  1. Add a cycle guard to _resolve_base_type_name for defence-in-depth (5-line change, low risk):

    def _resolve_base_type_name(self, name: str, seen: set[str] | None = None) -> str:
        if seen is None:
            seen = set()
        if name in seen or name not in self._type_aliases:
            return name
        seen.add(name)
        # ... existing logic, threading `seen` through recursive calls
  2. Migrate remaining callers onto _canonical_named_type — strictly aligned with the Cross-cutting: centralise canonical Vera-type-name resolution to close the #602 bug class structurally #630 goal but a bigger surgery (the call sites use string→string, the new walker uses TypeExpr→NamedType). Probably too large for a single follow-up.

Option 1 is the cheap defensive move. File only if there's appetite.

Verification needed

Before either fix, confirm whether the type checker rejects cyclic aliases. If yes, this whole concern is purely belt-and-braces. If no, the latent bug is real and Option 1 should land.

Pairs with

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions