Skip to content

-mm=own: strings as values (spec 22) - #452

Merged
ASDAlexander77 merged 16 commits into
mainfrom
own-strings-as-values
Oct 3, 2026
Merged

ASDAlexander77 merged 16 commits into
mainfrom
own-strings-as-values

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

-mm=own: strings as values (spec §22)

Under -mm=own, a second reference to a string that own cannot prove to be a move or a borrow
becomes a copy (ts.StringCopy). Copies are made only in a function the analysis rejects, and only
for the string retains a dry run of the analysis could not erase, so a function that compiles on
main compiles to the same IR.

  • ts.StringCopy and its lowering, under every model (null stays null; an optional is copied only
    when set).
  • isCopyableString: string, a string literal type, or an optional or a tag-free union of one
    with null/undefined.
  • The inference pass's dry run: the whole analysis, reporting and changing nothing.
  • makeStringCopies / copyForRetain: a string retain whose single use is in its block becomes a
    copy for that use; no copy for a parameter whose callers know it is kept or borrowed, for one
    operand taken twice, or for a use in another block.

Corpus (test/tester/tests, --emit=llvm -mm=own --no-default-lib)

main this branch gained lost
plain 417 / 593 453 / 593 36 0
--opt 416 / 593 451 / 593 35 0
first error main this branch
takes a second reference (the catch-all) 25 (27) 19 (22)
used after its value was moved 23 (25) 24 (25)
borrows a field and cannot be stored, returned or captured 20 11
returns a borrow of its argument on some paths and another value on others 17 1
borrows an element and cannot be stored, returned or captured 10 3
borrows a field but is used here after it may be released or overwritten 6 13
borrows an element but is used here after it may be released or overwritten 0 2
merged from several branches 8 (7) 5 (4)
borrows the global and cannot be stored, returned or captured 8 7
the rest 59 (57) 55 (54)

(--opt in brackets where it differs.) No error is new: every error this branch reports, plain,
--opt and --di, main reports too.

  • Gated: of the 35 files gained under --opt, 30 pass test-runner -mm=own AOT and JIT, with the
    default flags and with -noopt. The other 5 are halves of four import/export pairs. Three pairs
    pass the way the suite runs them. import_type_alias is still rejected by an error main has too.
  • Unchanged where it compiles: the 417 files that compile on main give identical LLVM IR from both
    binaries, once the names that change from run to run are normalised.
  • Measured (AOT, MB, a loop of r.s = "v" + i; r.t = r.s): own 4.4 / 4.4 at 20000 / 200000
    iterations, rc 4.4 / 4.4, none 6.6 / 25.9, gc 5.9 / 5.9.

Tests

  • Positive: own_string_copy_field, _after_move, _nullable, _exported, _mixed_return,
    _branch (own, rc, none, gc; AOT and JIT), and own_string_copy_written (own only).
  • test-own-string-copy-in-store-block: the copy for a store in a branch is made in the branch's
    block, not the entry block (a copy there would leak on the other path; test-runner cannot see it).
  • Negative: own_err_string_kept_param_known, own_err_string_call_keeps_twice,
    own_err_string_pushed_twice.
  • Four string negatives that compile now use a class; own_err_unbox_string is deleted.
  • Windows: ctest -C Release -R own 490/490. Linux (GCC, WSL): own-err|own-no-counting|own-verify|own_string_copy|own_err_string 191/191.

Full results, teeth and the shapes outside the rule: spec §22.7.

🤖 Generated with Claude Code

ASDAlexander77 and others added 16 commits October 2, 2026 22:28
A second reference to a string that own cannot prove a move or a borrow becomes a copy (ts.StringCopy):
silent, made only in a function the analysis would reject, and only for the string retains it could
not erase. A dry run decides where; the real run verifies.

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>
…checks its operand type

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mes a copy (spec 22.3, 22.4)

After a failing dry run, each string retain it did not erase is rewritten into a ts.StringCopy for
its one use (a `let` starts from a copy), unless the value is a parameter whose callers were told
this function keeps it or returns a borrow of it. The real run then decides the function.

Beyond the plan's code:
- ts.StringCopy is a borrow of its operand (isBorrow), and walkBorrowed does not follow its
  result: the copy is a block of its own.
- The copy is made at rc's retain when the use's operand is already there, not in front of the
  use: a store releases what it overwrites first, which may be the very string (`b.s = b.s`) or
  one the place check cannot tell from it (`b.s = a.s`, same field of another object).

Tests that differ from the plan:
- own_string_copy_field: no caught object (catch variables are not tested); `textOf` returns a
  field of an object it makes, since a field of its parameter is a borrow its callers rely on
  (`__own_result_borrows`, spec 22.4).
- own_string_copy_nullable: no `keepOpt`: passing a field's string to a callee known to keep it is
  the kept-parameter negative's shape (spec 22.4, 22.6).
- own_err_string_kept_param_known: `keep` uses its kept parameter after storing it, so the retain
  survives the dry run and the guard decides it; message "is used here after its value was moved".
  The plan's version (main passing a field's string) has no retain to copy at all.
- own_err_string_call_keeps_twice: "borrows a field and cannot be stored, returned or captured".
- own_err_string_consumed_twice dropped: it compiles before this change, and every indirect-call,
  push and store variant tried had rc's retains.
- own_err_mixed_return, own_err_param_returned_exported, own_err_param_kept_exported and
  own_err_merged_result use a class now; own_err_unbox_string is deleted.

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

A copy made at rc's birth retain, right after the producer, had no owner on a path that does not
reach its use in another block (`if (c) h.a = s;`): a leak. A retain is now copied only when its
use is in the same block; rc's retain for the store beside it makes the copy there, and the birth
retain goes in the real run once the value is proven. A store in a loop of a string made outside
it now copies on each pass, the old copy released by the store.

- own_string_copy_after_move: a store on one branch only, and one in a loop.
- own_err_field_loop: a class now; the string there is copied.
- own_err_string_pushed_twice: `list.push(s, s)`, two takers for one retain: no copy.
- own_err_string_call_keeps_twice: the header says why there is no copy (no retain in `main`).
- makeStringCopies returns nothing; copyForRetain says how its walk differs from takerOf's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
own_string_copy_mixed_return: a function that returns its argument on one path and a new string on
another, `<string>` of an `any` and of a `number | string`. Its eight borrowed returns are copied;
main rejects it with eight errors. No new code was needed. It runs under own, rc, none and gc.

Corpus, every file of test/tester/tests under own: 417 -> 453 of 593 plain, 416 -> 451 with
--opt, none lost. No error is new: each error the branch reports, plain, --opt and --di, main
reports too. Of the 35 gained under --opt, 30 pass test-runner -mm=own AOT and JIT, default and
-noopt; the other 5 are halves of four import/export pairs, three of which pass as the suite runs
them. import_type_alias is still rejected by an error main has too (an object literal re-made into
its annotated type, with no retain to copy). The 417 files that compile on main give the same LLVM
IR from both binaries, up to names that change between runs. A loop copying a field's string
stays at 4.4 MB at 20000 and 200000 iterations under own, as under rc.

- spec 22.7: what was built, where the copy goes, the corpus, gates, teeth, measures, the IR
  check, and the shapes outside the rule; the 21.2 table notes own_err_unbox_string's deletion.
- own_err_string_pushed_twice: its header says it pins a conservative rule.
- plan: boxes ticked, but Linux and the PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GCC in WSL, at efd252b: a clean build; own-err, own-no-counting and own-verify 140/140, and the
string-copy positives 48/48. The plan's last two boxes are ticked; the push and the PR are left
to the user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dry run's attributes were taken off before makeStringCopies, so a store
into a borrowing closure's box (or a borrowing field) lost its
__own_capture_borrow and counted as a second taker: no copy, and a false
"used here after its value was moved". They now come off after the copies,
before the real run. The copies erase only retains, which carry none of
them.

own_string_copy_after_move gains the shape: a closure that captures a let
(so borrows all it captures) and a string, the string then stored into a
field and used after. Spec 22.3 and 22.7 now say the attributes are set,
tracked and taken off after the copies; 22.7 also records why a closure
that owns its copies plus a field store stays an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same-block guard in copyForRetain had no test: without it, `stored(c)`
gets its copy at the entry block's birth retain, and the copy leaks on the
`c == false` path, which test-runner cannot see. test-own-string-copy-in-store-block
reads the own IR of own_string_copy_after_move.ts (own-string-copy-block.cmake)
and requires `stored`'s one ts.StringCopy after the entry block's cf.cond_br.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the same-block rule off, own_string_copy_after_move is rejected
first by its storedInLoop (a copy at the birth retain is then moved inside
the loop), so the IR check failed on the compile, not on where the copy
goes. own_string_copy_branch.ts holds `stored` alone: with the rule off it
compiles, and the check fails because the copy is in the entry block. It
is also a positive under own, rc, none and gc. Spec 22.7: the test, and
teeth (e).

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

`dominance` pointed at analyze's local DominanceInfo after it returned;
a scope exit now clears it, so nothing between or after the runs can read
a dead one. copyForRetain, building the copy at the retain, now requires
the retain to come before the use that reads the copy, and leaves the
error otherwise.

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

Spec 22.7: the IR check's normalisations as their exact sed patterns; the
Linux run names what came after efd252b; outside the rule, a copy at a
birth retain before a call that may throw (leaks on the throw, as the
original already does), and why isCopyableString's default CompileOptions
is safe against the lowering's real ones. 22.1 and 22.2 reflowed. Plan:
Task 4 Step 7 says the push and PR are left to the user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it (spec 22.6, 22.7)

The outside-the-rule bullet now names all three conditions under which a
closure with copies only borrows them (no escape, a bounded maker, no fresh
copy); 22.6 lists own_string_copy_branch.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 8b9d631 into main Oct 3, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the own-strings-as-values branch October 3, 2026 00:20
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