Skip to content

-mm=own: @nodrops C declarations and @final classes, toward an own default library - #611

Merged
ASDAlexander77 merged 11 commits into
mainfrom
own-final-classes
Oct 19, 2026
Merged

ASDAlexander77 merged 11 commits into
mainfrom
own-final-classes

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Toward building the default library under -mm=own: it had 62 ownership errors in lib.ts. With this PR and ASDAlexander77/TypeScriptCompilerDefaultLib branch own-default-lib, 5 remain (release; 7 with --di). All 5 are in matchAll, String[Symbol.iterator] and Response.constructor. Those are exported functions that keep or borrow a parameter, which needs a separate decision.

@nodrops C declarations (f25aaf8)

  • @nodrops declare function f(...) marks a C function that destroys nothing a TypeScript caller can reach. The ownership signature pass resolves a call to it the way it resolves an imported own library's no-drops function. Before this, every body that reached strncmp, boundedCopy and the like counted as "may destroy anything".
  • Four own false errors that the library hit:
    • Returning one local on two paths (if (c) return r; return r;) failed for every type. It now moves once on each path.
    • A string concat no longer extends a borrow of its operands (v[i] = v[i] + s).
    • ts.Assert only reads its message.
    • return [...a] now compiles.

@final classes

Spec: tslang/docs/superpowers/specs/2026-10-19-final-classes-design.md. Plan: tslang/docs/superpowers/plans/2026-10-19-final-classes.md.

  • No class may extend a @final class. That includes from another module, because __decls keeps @final. @final abstract and @final @cpp are errors.
  • Direct calls. A method or accessor of a value whose static type is a @final class is called directly (ThisSymbolRef), not through the vtable. This applies only when the member's class is defined in this module. Members of imported or declared classes keep vtable calls: a static importer can't link a member of a base the library doesn't export. The vtable, interfaces and instanceof are unchanged.
  • No posing. An unrelated class that fits a @final class member by member is no longer assignable to it.
  • Bugs the direct calls exposed:
    • A method read as a value (const fn = f.m) had no lowering for ThisSymbolRef.
    • Generator methods of exported classes were not exported: the wrapper dropped DllExport/IsPublic.
    • Under own with --di, a cast to null (how --di records a variable narrowed by == null) counted as taking a reference.

Tests

  • New compiler tests:
    • final/ error cases.
    • final_dispatch under gc, rc, none and own, AOT and JIT.
    • IR checks for direct and base calls.
    • A -shared export/import pair, including a @static import of a @final class with a non-exported base.
    • A cross-module extends error.
    • Own tests: own_final_*, own_declared_no_drops, own_return_paths, own_string_concat_element, own_null_narrowing_di.
    • test-export-generator-method and test-compile-class-cast-to-final-error.
  • Full Release suite: 4714/4714.
  • DefaultLib (branch own-default-lib): release gc and rc builds pass tests.ps1, compiled and JIT.

🤖 Generated with Claude Code

ASDAlexander77 and others added 10 commits October 19, 2026 09:29
…brary hit

Toward building the default library under -mm=own (62 errors in lib.ts, now 14):

- `@nodrops declare function f(...)`: a C function that destroys nothing a TypeScript
  caller can reach. The signature pass resolves a call to it as it resolves one to an
  imported own library's no-drops function, so a borrow of a field can be lent to it.
  A declaration had no facts, so every body that reached strncmp, boundedCopy and the
  like might destroy anything.
- An owning local returned on more than one way out (`if (c) return r; return r;`)
  moves once on each: receivers no path joins are decided alone, as the receivers
  separated by assignments already were.
- A string concatenation reads its operands into a block of its own: its result is no
  longer walked as the borrow (`v[i] = v[i] + s`).
- `ts.Assert` only reads its message.
- `return [...a]`: the spread's builder is moved out by the return, as it is moved into
  the `let` it is read into.

Tests: own_declared_no_drops, own_return_paths, own_string_concat_element (run under
own, AOT and JIT), own_err_declared_drops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A method or accessor of a value whose static type is a @Final class is called through ThisSymbolRef, not the vtable; the receiver's finality is carried through the walk to the base classes. A method read as a value (const fn = f.m) is bound by a new ThisSymbolRef lowering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A @Final class's method result is fresh and a field may be lent to it: both error without @Final. No ownership pass needed a change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An importer calls a @Final class's methods directly and links (import library and the JIT's run-time DLL), and cannot extend it: __decls keeps @Final.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With --di, the variable that if (r == null) narrows is recorded as a cast of the class value to null, which holds no block; it was read as a second reference to a fresh call result. Found building the default library's String.match with debug info.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The generator wrapper declaration dropped the DllExport/DllImport/IsPublic flags the class gave the method, so a public generator method was private. Through the vtable nobody noticed; a direct call on a @Final class (the default library's String[Symbol.iterator] under the JIT) found no symbol.

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

From the final review: a static importer of a @Final class whose base its library does not export failed to link (the inherited member has no symbol in the import library), so a member is called directly only when its class is defined in this module. And a class that fits a @Final class member by member is no longer assignable to it: the direct call would run the @Final class's body on the other class's fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It checks the IR for dllexport, which ELF does not have; the Linux CI job failed on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit ccd828c into main Oct 19, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the own-final-classes branch October 19, 2026 10:45
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