Skip to content

-mm=own: @record beside @lends carries a borrowing record across modules - #680

Merged
ASDAlexander77 merged 2 commits into
mainfrom
own-record-marker
Oct 23, 2026
Merged

ASDAlexander77 merged 2 commits into
mainfrom
own-record-marker

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

What

A new @record marker, written beside @lends. It says the result is a borrowing record (own spec 32): a box the caller owns that holds a borrow of each argument @lends names. Without it, @lends alone means a plain borrow of an argument. Another module sees only the declaration, so the marker is the only way it can tell the two apart.

@lends("a", "b")
@record
export function pairOf(a: Node, b: Node): Pair {
    return { a: a, b: b };
}

Why

Spec 32 documented a leak: "A declaration's @lends, seen from another module without its body, is a plain borrow: a record's box then leaks at that caller." The importer could not tell, and inferring record-ness from the result type, or from @lends naming several parameters, is not safe. A plain borrow of an interface argument looks the same, and a caller told it owns a record would free that argument, which is a double free rather than a leak.

How

  • MLIRGen reads @record on functions and methods into ts.own_declared_record, a unit attribute. The declaration printer emits it after @lends, and the dynamic-import global carries it.
  • Signature pass:
    • meetFacts gives a declaration's calls __own_result_record when the declaration says @record.
    • calleesDeclare does the same for a call nothing resolves.
  • OwnedReturnConsumptionPass: under own, a call whose callee declares @record gets an owned result (calleeDeclaresRecord). The callee may be named directly, read through a dynamic import's global, or called as a method. This half is what closes the leak. MLIRGen never treats an imported call's result as owned, so before this change the importer emitted no release of the box at all.
  • checkDeclared, in the defining module:
    • @record without @lends is an error.
    • @record on a body that returns its argument as it is, is an error.
    • An exported function of a library (a module that is not an executable) whose body returns a record must say @record. A function no other module calls does not need to: its callers go by its body.
    • An override must say the same @record as what it overrides; the pinned message of own_err_marker_override now lists @record.
  • Spec 32 is updated.

Tests

  • export_own_record / import_own_record run with -shared under gc, rc and own, covering both a function and a method that return records.
  • import_own_record_outlives checks that reading the record after an argument it borrows is reassigned is still refused.
  • own-shared-record.cmake (Windows) checks that the importer's IR releases each Pair box. Before the consumption-pass change there was no release.
  • New negatives: own_err_record_without_lends, own_err_record_not_record, own_err_record_exported_unsaid.
  • own_lends.ts exports pairOf, a record function, so it now says @record. The DefaultLib uses no @lends.
  • Full suite: Windows 5507/5507, Linux 5485/5485.

Not covered

A call to another module's method that isn't on the library's no-drops list counts as one that may free what the caller borrows. So in the importer, a record still can't be read across such a call. That is unchanged by this PR; import_own_record calls the function and the method in separate loops so the two don't interact.

🤖 Generated with Claude Code

ASDAlexander77 and others added 2 commits October 23, 2026 23:02
…modules

A declaration seen from another module has no body to say whether its result
is a borrowing record (own spec 32) - a box the caller owns - or a plain
borrow of an argument. The importer took it for a plain borrow and never freed
the box. `@record` beside `@lends` says it:

- MLIRGen reads it (OWN_DECLARED_RECORD_ATTR_NAME, a unit attribute) on
  functions and methods; the declaration printer emits it after `@lends`.
- The importer's declaration has it: meetFacts and, for a call nothing
  resolves, calleesDeclare give the call `__own_result_record`, and under own
  OwnedReturnConsumptionPass counts its result owned (calleeDeclaresRecord: by
  name, through a dynamic import's global, or as a method), so the box is
  released where the caller lets it go. Before, no release was emitted at all.
- checkDeclared: `@record` without `@lends`, or on a body that returns a borrow
  as it is, is an error; an exported function of a library whose body returns a
  record has to say it. An override says the same `@record`.

Guessing record-ness from the result type or from several `@lends` names is
not safe: a caller told it owns a record would free an argument.

Spec 32 updated. Tests: export_own_record / import_own_record (-shared, gc rc
own), import_own_record_outlives, own-shared-record.cmake (the importer
releases each box), three negatives; own_lends's exported pairOf says @record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	tslang/test/tester/CMakeLists.txt
@ASDAlexander77
ASDAlexander77 merged commit 38697c4 into main Oct 23, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the own-record-marker branch October 23, 2026 22:43
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