Skip to content

A type alias that contains itself is an error, not a stack overflow - #411

Merged
ASDAlexander77 merged 2 commits into
mainfrom
fix-recursive-type-alias
Sep 30, 2026
Merged

ASDAlexander77 merged 2 commits into
mainfrom
fix-recursive-type-alias

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Fixes #370: a type alias that refers to itself crashed the compiler with a stack overflow (0xC00000FD, no message). It's now a compile error.

Cause

Every mention of a type alias expanded its declaration again, and nothing noticed when the same alias was already being expanded higher up the stack.

Fix

  • Detection: an alias being expanded is remembered (expandTypeAlias). This covers the plain resolution and both generic ones: resolveTypeByNameInNamespace, getTypeByTypeReference and resolveGenericTypeInNamespace. An alias that meets itself is reported:
    error: type alias 'Node' circularly references itself; use an interface or a class for a recursive type
    
  • Why an error, not a recursive type: a type here has a fixed shape, because a tuple holds its fields in place. So no alias can contain itself, not even behind Reference<T>: its tuple type would have to name itself, and the dialect's tuples are structural. A real recursive alias would need a tuple type that can refer to itself, which is a separate design. An interface or a class is the recursive type today. tsbindgen's rule of mapping self-pointers to Opaque therefore stays.
  • One error only: errors that merely follow from it are left out. Those raised while the expansion unwinds are swallowed by a scoped handler until the outermost expansion of that alias ends. At later mentions, "can't find type", "can't resolve name" and "generic type … can't be found" are skipped for an alias already reported.
  • Exported aliases: an exported alias is now registered before it is resolved, so it is found, and found circular, the same way.
  • Aside: the old code tried to cache the resolved alias, but its condition was inverted and it wrote into a copy, so aliases were never cached. The dead code is gone. Aliases stay uncached, which is effectively what happened before, because a resolution can differ between MLIRGen passes.

Tests

  • type-alias-circular/ has nine shapes, each passing only on the message and failing on the follow-on messages or a stack dump:
    • a tuple through Reference;
    • an object;
    • an optional field;
    • a declared function's parameter;
    • two mutually recursive aliases;
    • an exported alias;
    • a generic List<T>;
    • a union through an array;
    • an alias inside a namespace.
  • positive.ts: aliases that name other aliases, and nested generic ones (Pair<Pair<number>>, Box<Box<T>>), run JIT and AOT.
  • Windows Release: 3264/3264.
  • Linux (WSL): 3250/3250.
  • DefaultLib rebuilt with this compiler: 157/157, compile and JIT (release, gc). The previous DefaultLib build was restored afterwards.

Also: debug build fix (separate commit)

MLIRGenCast.cpp didn't compile in a Debug build after #407. The MLIRRTTIHelperVC.h include undefines DEBUG_TYPE, and the file uses LLVM_DEBUG. It now defines DEBUG_TYPE again after that include, as MLIRGenStatements.cpp already does.

🤖 Generated with Claude Code

ASDAlexander77 and others added 2 commits September 30, 2026 00:44
`type Node = [value: s32, next: Reference<Node>]`, `{ value: number,
next: Node }`, mutual aliases, a generic `List<T>` inside itself: every
mention of an alias expanded its declaration again, and one that names
itself did so until the stack ran out - exit 0xC00000FD, no message.

An alias being expanded is now remembered (expandTypeAlias, used by the
plain and both generic resolutions), and one that meets itself is
reported: "type alias 'Node' circularly references itself; use an
interface or a class for a recursive type". A type here has a fixed shape
- a tuple holds its fields in place - so none can contain itself, not
even behind Reference<T>, whose tuple type would have to name itself; an
interface or a class is the recursive type.

The errors that only follow from it are left out: those raised while the
expansion unwinds (a scoped handler swallows them until the outermost
expansion of that alias ends), and "can't find type" / "can't resolve
name" / "generic type ... can't be found" at later mentions of it. An
exported alias is registered before it is resolved, so it is found - and
found circular - the same way.

Tests: nine shapes in type-alias-circular/ (tuple through Reference,
object, optional field, declared function parameter, mutual, exported,
generic, union through an array, in a namespace), each passing only on
the message and failing on the follow-on ones; and positive.ts, aliases
naming other aliases and themselves one after another, JIT and AOT.

Fixes #370.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…helper

#407 included MLIRRTTIHelperVC.h after MLIRGenImpl.h, and the helper
undefines DEBUG_TYPE on the way out, so every LLVM_DEBUG in the file was
an error in a Debug build (Release compiles LLVM_DEBUG away, so CI never
saw it). MLIRGenStatements.cpp does the same after the same include.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 8d3c90f into main Sep 30, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-recursive-type-alias branch September 30, 2026 00:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Self-referencing type alias crashes the compiler (stack overflow, no message)

1 participant