Skip to content

A class field's function type inferred from its initializer works - #616

Merged
ASDAlexander77 merged 1 commit into
mainfrom
fix-class-field-inferred-function-type
Oct 19, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
fix-class-field-inferred-function-type

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Two class-field bugs with no interface involved, found while landing #612.

1. fn = () => 0 then a capturing closure

class W2 { fn = () => 0; constructor(k: int) { this.fn = () => k; } }
function main() { const w = new W2(9); print(w.fn()); }

warned "losing this reference" and crashed (0x80000003). The field's type was inferred from the initializer as a plain !ts.func, which has no room for a closure's context. A field whose function type is inferred is now stored as a hybrid function, as an annotated fn: () => int field and a let (adjustLocalVariableType) already are.

2. fn = this.m

class W3 { v = 4; m(): int { return this.v; } fn = this.m; }
function main() { const w = new W3(); print(w.fn()); }

failed with "not a function to call". The real pass types the field as the bound method, but the discovery pass lays out the fields before it declares the methods, so the field was any there and the call failed before the real pass ran. In a discovery pass such a field now holds its place and is typed again once the members are declared (mlirGenClassRetypeFields). Code emitted against the placeholder is thrown away with the pass.

The field type computation moved into mlirGenClassDataFieldType so that both places use it.

Tests

00class_function_typed_field_inferred.ts covers both forms, reassignment (including a closure replacing the bound method) and the interface path from #612. It runs under gc AOT/JIT and in the rc/none corpus. Before the fix it failed in all 6 modes; it now passes in all of them. Full ctest -C Release passes: 4736/4736.

These also work: a class used before its declaration, a class local to a function, and fn = this.m naming an inherited method. Under -mm=rc --verify-ownership the test produces no diagnostics.

🤖 Generated with Claude Code

Two class-field bugs with no interface involved:

- `fn = () => 0` was stored as a plain function, which has no room for a
  closure's context: assigning a capturing closure to it later warned
  "losing this reference" and the call crashed (0x80000003). A field whose
  function type is inferred is now stored as a hybrid function, as an
  annotated `fn: () => int` field and a `let` (adjustLocalVariableType) are.

- `fn = this.m` had no type in the discovery pass: the fields are laid out
  before the methods are declared, so the field became `any` there and
  `w.fn()` failed with "not a function to call" before the real pass ran.
  Such a field now holds its place and is typed again once the members are
  declared (mlirGenClassRetypeFields); the code made against the placeholder
  is thrown away with the pass. The real pass already typed it as the bound
  method.

The field type computation moved into mlirGenClassDataFieldType so both
places share it. 00class_function_typed_field_inferred.ts covers both forms,
reassignment, a closure over the bound-method field and the interface path
of #612, under gc AOT/JIT and the rc/none corpus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 46698f6 into main Oct 19, 2026
3 of 4 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-class-field-inferred-function-type branch October 19, 2026 16:10
ASDAlexander77 added a commit that referenced this pull request Oct 19, 2026
CMakeLists.txt: both sides added a test at the same place; both kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant