Repository navigation
A global lent to a call that assigns it: rc holds a count, own rejects - #639
Merged
Merged
Conversation
`function run(h: C) { G = mk(); print(h.x); }` called as `run(G)` (or
`G.m()` with `m` assigning `G`) freed the object the parameter still read:
an argument is borrowed, and the callee does not know its parameter is
the global's.
- rc: the caller holds a count across the call for an argument read out
of a global, or a field or an element of one (`holdGlobalArguments`).
Not for a call in a try body (ts.Invoke ends its block) nor a Shared<T>
handle, whose count a program reads.
- own: such a global argument given to a call that may drop is an error
when anything assigns that global (`rejectGlobalArgumentsDropped`). A
field of a global given that way was already rejected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # tslang/lib/TypeScript/OwnershipSignaturePass.cpp
ASDAlexander77
added a commit
that referenced
this pull request
Oct 22, 2026
…unt (#643) * A Shared global lent to a call that assigns it: the caller holds a count `run(bare)`, with `bare` a `Shared<T>` global and `run` giving `bare` a new value, freed the block the parameter still read, under rc and own alike: #639's hold of a global argument across a call left handles out, since a held count shows in `Shared.count`. A handle is now held, under rc and own, across a call that may assign the global it was read from - directly, in a callee, or through a call that names no function - and only there, so `Shared.count` elsewhere is still the number of handles (spec 23.1); no existing count changes. Under rc a handle read out of a global's field is held the same way, and so is a value read through a global handle (`run(G.value)`); under own a read out of a field stays a borrow, rejected as before. The comment about calls in a try body is corrected: this pass runs before the affine lowering makes them `ts.Invoke`, so they were already held on the normal path. A throw out of such a call skips the release (one count leaked), which is left open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test a value read through a global handle across a call that replaces it `run(G.value)`, with `run` doing `G.value = mk(..)`, failed on main under rc and is held by this branch; the test now pins it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77
added a commit
that referenced
this pull request
Oct 22, 2026
holdGlobalArguments (#639, #643) retains a global read given to a call that may overwrite it and releases it after the call. A throw out of the call skipped the release: a try around the call caught the exception, and the global's block kept a count nobody gave back - under rc for any held value, under own and rc for a Shared<T> handle, where `Shared.count` showed it. When a try in the same function takes the call's exceptions, the held value now also goes into a slot made at the function's entry, null outside the call, and the handler that runs first (the try's cleanup, else its catch clause, else its finally; the finally for a call in a catch clause - the landing pad's own order) begins with a release of that slot. An exception that leaves the function still leaks the count, as it does for locals. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 22, 2026
ASDAlexander77
added a commit
that referenced
this pull request
Oct 23, 2026
…global a count (#647) Two shapes the memory-model fuzzer (#646) found double-freeing under rc since #639. - holdGlobalArguments retained the argument right before the call. A later argument of the same call could give the global a new value first (`f(G, (G = mk(), x))`, `f(H.n, (H.n = mk(), x))`): its release freed the block, and the hold then retained and released a freed block. The retain (and the #645 unwind slot) now follow the read. - `const n = G` was folded into the read, holding no count. Once the global got a new value, here or in a call, `n` dangled (fuzzer R1). #639 made that a crash: `run(n.v)`, with `run` assigning G, held and released a field of the freed block. Under rc a const read out of a global, or an interface or bound method made over one, now gets storage like a const read out of a field or an element (#635, #641, #642, #644). Under own the folded read stays a borrow. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77
added a commit
that referenced
this pull request
Oct 23, 2026
* Design: -mm=own compiles the ray tracer
Two source edits in an own copy of the benchmark (Shared<Surface>,
Plane.normal returns a copy) and nine compiler gaps to close: immortal
globals, borrowing records, owning literals, Shared of a literal, field
reads into a let, read after a move, facts through private interfaces,
reduce, and leftovers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Plan: -mm=own compiles the ray tracer
Nine tasks, one PR each: immortal globals, Shared of a literal, owning
literals, field reads into a let, read after a move, facts through
private interfaces, borrowing records, reduce, and the ray tracer test
and benchmark row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T1: -mm=own: a global never reassigned is immortal
A class-typed global of this module, not exported, written once by its
initializer with a fresh object (or another immortal global's value), is
immortal under own: the initializing store marks its block
HEAP_BLOCK_IMMORTAL, so every release skips it, and a read of it owns
nothing - it may be returned, stored into a field or another global,
given to a call that keeps it, or merged with a fresh value.
- OwnershipSignaturePass.findImmortalGlobals: region initializers and a
class static's one store in its static constructor; every symbol use
checked (a static written through a vtable reference stays mortal).
Pins __own_immortal on the global, its AddressOfs and the init store.
- own_facts::isImmortalGlobalRead; inference treats it as
isImmortalLiteral and erases its retains; moveIntoGlobals too.
- Lowering: the marked store writes the header word; a marked region
global always becomes a __cctor so that store exists.
Tests: own_immortal_global, own_immortal_global_region (own/rc/gc/none),
own_err_immortal_borrowed_init, own_err_immortal_field_overwritten;
own_err_global_returned and own_err_param_kept_not_owned now reassign
their global. Spec section 25.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T1: no call's result makes a global immortal
findImmortalGlobals ran isFresh before the callees' facts were known, so a
global given a call's result was marked even when the result borrows: a
temporary the region frees (the header was then written into a freed
block, and the global held it), or an element or field another value owns
(its free became a silent leak). Now only an object made right there and
not given back with a ts.Release, nothing, or another immortal global's
value qualifies; such globals stay mortal and keep today's errors.
New negatives own_err_immortal_region_call (const c = id(new V(1))) and
own_err_immortal_region_borrow (const c = firstOf(list)).
own_immortal_global declares its sum as number (no s32 warning).
Spec section 25 updated, including the older region-form gap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T2: -mm=own: an object literal moves into new Shared in a global
moveIntoGlobals judged rc's retain of the payload given to ts.SharedNew and found it
not going into the global's ts.GlobalResult: "the initializer of global 'g' takes a
second reference". movesIntoResult now takes new Shared(x)'s payload operand as a
move: the handle's block owns it. The value must still be fresh, retained once and
released by nothing in the region, so another handle's payload keeps the error
(own_err_shared_literal_borrowed). A local already compiled: the inference took the
retain as the move. Calling a function field through a handle stays an error: a call
through an interface slot has no facts (G9). Spec section 26.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T3: -mm=own: an owning object literal moves anywhere a fresh object does
An object literal given as an object or an interface is copied into a box (a ts.New of a
value_ref). recordRetainOf now takes that copy as the record's one owner, so the fresh values
stored into its fields move in at the record's retain and the box's own check decides where
it goes: a call argument (released after a borrowing callee, taken by a keeping one), an array
literal element, a nested literal's field, a local through an interface view, the result.
own_facts::isOwningRecord names the record (its read, its box, or a view of the box) whose
owning fields each hold a fresh value or no block; a let read or a borrow keeps the error.
own_err_record_literal_given becomes the positive own_record_literal_given; new
own_literal_moves, own_err_literal_moved_twice, own_err_record_literal_let.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T3: spec 27, owning literals moved anywhere
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T3: -mm=own: a method reached through an interface may not keep a parameter
A class's interface vtable (`<class>.<interface>..vtbl`) holds the class's `..instanceOf`, so
the signature pass took it for a class vtable and its methods for calls it resolves. A call
through the interface gets none of their facts, so the caller released what the method kept: a
double free (`s.take(new V(1))`), newly reachable with a literal argument. Such a method that
keeps a parameter (or says @keeps) now loses its facts and its body is the existing error
("is a parameter, kept here, but -mm=own cannot make the callers of ... give it up"). G9 relaxes
it for closed interfaces.
New negatives own_err_interface_keeps_literal, own_err_interface_keeps_param; spec 21.2 row and
27 updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T3: -mm=own: a method reached through an interface may not return a borrow
The mirror of the keeping rule: a call through an interface gets none of the method's facts, so
a caller released a result that borrows an argument or `this` (`g.get(a)`, `i.first()`,
`h.hold()`) as its own, a double free. A method an interface vtable holds now loses borrowing
facts too (body or @lends), and its body is the existing error. The interface reason wins over
an exported method's "@lends or @keeps would say it" note.
New negatives own_err_interface_borrow_param, _element, _field; spec 27 updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T4: -mm=own: a field read through an interface is a place read
`let d = r.dir` with `r: R` an interface failed with "takes a second
reference": a ts.InterfaceSymbolRef field was not a place, so its read
was not a borrow of the container. The same gap left `const` reads
through an interface unchecked (overwritten through the interface, then
read: use after free) and overwrites through an interface invisible to
both passes (a class field's borrow outlived `hi.c = ...`, and a callee
writing through an interface parameter was given `__own_no_drops`).
An interface field (InterfaceSymbolRef with a reference result) is now
a place: its read borrows, bounded by what it was read through (a chain
of field reads and interface views), and ended by a call only when the
call's facts say it may drop. Its position in the object is not known,
so an overwrite through an interface drops every field on a chain and
any field overwrite drops a read through an interface.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T4: spec 28, a field read through an interface
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T5: -mm=own: a read after a move goes through the new owner
A use of a moved value that only reads it - a field, element or length
read, a method reached through it, a comparison, print/assert, an
argument the callee borrows - is a borrow of the place the value moved
into: an owning local, or a field or element of a local or `this`.
It is bounded the way checkPlaceRead bounds a read of that place: the
ends of the place's roots and the drops that may destroy what it holds
(an overwrite, through an interface view too; a call whose facts may
drop; a call given a closure), on a path from the move to the read on
which the value is neither made again nor moved again. Any other use
after a move is still a second owner.
own_read_after_move runs straight-line moves into fields, a move into
an owning local in a loop that makes the value each iteration, and a
declaration that takes a value over, under own, rc, gc and none.
New negatives: overwritten on one path, through an interface view, by
a callee that may drop, around a loop's back edge, and after a loop.
own_err_field_alias and own_err_union_moved now overwrite the place.
own_err_borrow_{bound_method,catch_local,finally_local} give `a` to two
lets: with one, the let now takes the value over and the program is
correct.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T5: spec 29, a read after a move
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T5: spec 29, teeth as measured; a store through a read
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T5: -mm=own: an assignment to a global ends a borrow of what it may free
`G = ...` releases the global's old value and all it owns, and a
parameter, `this`, or a value read out of a field may be that value.
The drops took only a release of a place or a cell, so a read through
`h.c` (or of a value moved into `h.c`) after `G = mkH()` was accepted
with `run(G)` and read a freed block. A ts.ReleaseSlot of a global is
now a drop, and dropsChain takes it for every chain whose root this
function does not own. An immortal global's one store is its
initializer, which frees nothing.
own_err_read_after_move_global (a parameter, `this`, a callee) and
own_err_field_let_global_overwritten pin it. own_read_after_move gains
an element and a `this`-field destination and takes its values from
factories; the loop negative churns; spec 29 records it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T5b: -mm=own: a move on some paths only releases on the others
`if (c) { best = v; }` left one release of `v` where the paths join,
on a path that moved the value and on one that did not. No run-time
record of the move is kept (spec 2.2), so the release is now placed
statically: a forward walk of the CFG gives what the local's slot may
hold before each op (nothing, its value, a moved value); every release
that may hold a moved value goes, and a release is put on each edge
from a block that ends holding the value into a block where the paths
have joined - at the end of the block when it has no other successor,
else in a new block on the edge. An assignment's own release over a
value that may have moved goes the same way.
The release moves earlier, so nothing may use the value where the
paths have joined: no read of the slot and nothing read out of it
(followed through results, block arguments, stores into locals and
regions; a call's result only when it borrows the argument). The rule
applies to a local's one receiver, outside exception handlers, on
cf.br/cf.cond_br edges; anything else keeps the error. Unwind edges
are CFG edges and get the same treatment.
own_conditional_move covers the brief's loop, straight-line moves taken
and not taken, a 100,000-iteration churn, an assignment over a maybe-
moved value, a move into a keeping call on one arm of ?:, and a move in
a try whose call throws; same output under own, rc, gc and none.
Negatives: a read after the join, through an overwritten owner, a field
borrow, an outer let in a loop, and a move in a catch.
own_err_reassign_some_paths and own_err_borrow_derived_ref became
correct programs and were changed to keep their errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T5b: spec 30, a move on some paths only
What the static release placement accepts and leaves out, its tests,
teeth, corpus (527 -> 528, 00nullish_values gained) and the ray
tracer (33 -> 31 errors: both moved-on-some-paths errors gone).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T5b: -mm=own: nothing read out of a released value is used after its edge
The walk of what is read out of a local that moved on some paths only
checked uses only where the paths have joined. After a new value is
stored into the local, or after its scope ends, a bound method or an
interface view of the old value went unchecked, and the release placed
on the edge freed what it read (review: `const m = v.get; if (c) { b =
v; } v = mkv(2); m()`). Now a use of anything read out of the value is
refused when it is reachable from the start of a block an edge released
the value into, before the read it came from (or the op that made the
value) runs again. A read of the slot itself after a new value is
stored reads that one, and stays accepted.
The error now carries a note at the use that kept the release from
being placed. own_conditional_move gains a move on the right of `&&`,
whose skipping edge forwards the `&&`'s value, so the release goes in a
block of its own that passes it on. Negatives: a bound method and an
interface view used after the join (only this walk sees them), and both
used after the local is given a new value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T5b: spec 30, uses past the join and the teeth that show them
What is read out of a released value may not be used after its edge,
not only where the paths join; the walk is the only check for a bound
method or an interface view, and its teeth; the && split edge; the
string remark; the no-move hole that stays open in every model.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T6: -mm=own: a call through a closed interface gets its implementations' facts
An interface only this module can see - not named by an exported declaration, not in the type of
a public or declared function or an exported global, every value of it made by ts.NewInterface
over a vtable the signature pass reads, every vtable written only by its initializer, no exported
class holding one - has, per vtable slot, a known family of functions. A call through the slot
(ts.InterfaceSymbolRef) is resolved to that family like a private virtual call: it gets
__own_no_drops when every member destroys nothing, and the kept parameters and borrowed result
its members agree on (meetFacts demotes a family that disagrees). Its members keep their facts,
for direct calls too: Task 3's stripping (interfaceReached) now applies only to members of open
families and to vtables no interface value is made with.
A cast into an interface from another interface opens both, one from `any` or an opaque pointer
opens every interface boxed into one, and an untraceable vtable opens every interface.
boundThis sees an interface-bound method's object, so a result that borrows `this` through the
interface is bounded by the interface value.
The five own_err_interface_* negatives of Task 3 now export their interface (the open form, same
texts); their closed forms are in own_interface_facts. New negatives: an exported implementation,
a family that disagrees, a member that drops, an interface made from another, a borrowed result
outliving its owner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T6: -mm=own: a call through a closed interface's function field gets its functions' facts
`p.s.f(3)`, `g.value.f(3)` with `f: (x: number) => number` an interface field: the slot of each
of the interface's vtables holds the offset of an object literal's field, and the call goes to
whatever function that field holds. When the interface is closed and every function ever stored
into those fields is one this module defines - the literal's own (its ts.Constant), or one stored
later through the object or through the interface - the call is resolved to them and gets their
facts (__own_no_drops when none destroys anything). A store of anything else, the field reached
other than to be read or stored, an object of the literal's type made other than by the literal,
a function-typed store through an open interface, or a class implementer's thunk cell keeps the
call open.
Exposure through a type is now read from exported and declared functions only: a `public` method
of a class no other module can name does not expose the interfaces its class's fields mention.
own_interface_fn_field (gc, rc, none, own; through a global and a local Shared<S> of a literal and
through a parameter, each after churn). Negatives: a field function that overwrites a field of
its object, one of unknown origin (a parameter), an exported interface.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T6: spec 31, facts through a closed interface
A vtable named for an interface that this pass could not read, or that no value of any interface
is made with, opens the interface (a member outside its families would get their facts). A cast
between references is judged by what they point to, so `bound_ref<T>` seen as `ref<T>` no longer
counts as a reinterpretation.
Spec 31: what a closed interface is, the families and their meet (agreement, a sound narrowing of
the plan's union of kept parameters), function-typed fields, tests, teeth, measurements, the
corpus, and why the ray tracer's errors stay at 31 (`sqrt`/`floor` declared without `@nodrops`,
`Surface` exported by `export let` in `module Surfaces`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T6: spec 31, public members and namespace exports
A `public` method is open (findOpen), so a family it is in agrees only on no facts; `export` inside
a namespace exports the declaration from the module, so such an interface is open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T6: -mm=own: closed interfaces after review - reinterpreted functions, C scalars, public methods, executables
Review fix round 1 of Task 6 (G9):
- A function value reinterpreted - out of `any`/`unknown`, or as a function of another signature
(`f2 as any as (o: Op, a: V) => number`, `(() => Op)` as `(() => Op2)`) - carries the interface
values of its parameters and results across unseen: every interface its signatures mention is
open, and those of a function boxed into `any` open with any unboxing (castReinterpretsFunction).
A bound function's `this` and an opaque `this` are not part of the comparison. Negatives
own_err_interface_fn_any_param / _result / _drops (review6 x1c, x1r, x1a).
- (ruling) A body-less C declaration with no export or import marker whose parameters and result
are numbers, booleans, characters or enums of them destroys nothing (scalarDeclaration):
`sqrt`, `floor`, `pow`, `fabs`. Positive own_declared_scalar_no_drops.
- (ruling) A `public` method of a class no other module can name - not exported, not a word of
`__decls`, not in the type of an exported or declared function or an exported or imported
global - is not callable from another module (callableFromOutside, used by findOpen and by class
families). own_err_generator_method_this now exports its class to keep its meaning. Negative
own_err_interface_public_exported; the public form is in own_interface_facts.
- (USER decision) An executable's exports are seen by no other module: the pass takes
`executable` (isExecutable or isJit; false when unknown), and then `__decls` words, exported
functions and globals, and exported class vtables do not open an interface. Imports still do
(a declared function, an imported global, a global with no initializer). own_interface_exe_exports
runs under the JIT and as `--emit=exe` (own-exe-exports.cmake), and stays an error as an object
file and a DLL.
- (ruling) An iterator `next()` through a closed interface gets the family's facts like any other
call; no fresh-result assumption for it.
- own_interface_facts' churn keeps its allocations alive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T6: spec 31 after review, and a public method reachable through an exported function
Negative own_err_interface_public_reachable: a `public` method of a class an exported function
returns keeps its error. Spec 31: reinterpreted functions, C scalar declarations, public methods
of classes no other module can name, executables' exports, iterator next(), teeth, corpus, the ray
tracer built as a program (31 -> 15 errors), and the rulings on declared markers and abstract
classes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T6: -mm=own: closed interfaces, re-review 1 - member types, overrides of outside bases, C callbacks
- An interface value made out of another interface, or out of `any`, opens every interface either
side's members mention (vtable slot types and what is read out of its values), recursively:
`HasF2 { f: (o: Op2, ...) }` seen as `HasF { f: (o: Op, ...) }` handed an `Op` to a call through
`Op2` (review6r1 v5, v11f). null seen as an interface makes nothing. Negatives
own_err_interface_member_types, own_err_interface_member_methods.
- A method of a class whose base chain holds a class another module can name, or one it defines
(no own vtable here), is callable from outside, private or not: that module calls it through the
base's vtable (review6r1 p5, p6, p7; the private form was accepted at BASE too). Negatives
own_err_override_nameable_base (library side), own_err_override_imported_base and
own_err_override_imported_class (importer side); the pairs run under rc, gc and none.
- The C-scalar no-drops rule is off in a module whose functions escape to C: cast to something
that is no function, or given to a declared function (review6r1 c3, `signal` then `raise`).
Negative own_err_declared_callback.
- Spec 31: after re-review.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T6: -mm=own: closed interfaces, re-review 2 - class-made members, C callbacks by type
- The member-type closure of interface-from-interface casts reads the class vtables too: they joined
the interface's vtables only after it, so an interface whose values are all made from class
objects (`const r2: Runner2 = rr`) had no member types (review6r2 d1-d3). Negatives
own_err_interface_member_class, own_err_interface_member_any.
- The C-scalar rule's escape test is by type (controller ruling): off in a module with a cast from a
type that holds a function to one that holds none (a vtable's address is the compiler's own; a
class or object output counts its fields), or a call of a declared function with an argument
whose type holds a function anywhere. The value-following walk missed a global initialized with
the function, a wrapper, a returned function, a method reference and a tuple (review6r2 k1-k10).
Negatives own_err_declared_callback_global, _method, _tuple.
- Spec 31: the wording of the C rule, fix round 3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T6: -mm=own: closed interfaces, re-review 3 - C callbacks through indirect calls
- The C-scalar rule's escape test judged a call by its direct callee only, so a declared
function called through a value (`let sg = signal; sg(15, onSig)`, a global holding it, a
wrapper's parameter, a C-returned pointer cast to a function), a `declare class` or `@cpp`
method and a foreign interface's member left the rule on (review6r3 n7b/n7d/n7e: own `77777 9`,
gc `5 9`; n7g, n8b, n8c, n9). A call now counts when `resolve` does not tie it to functions
defined here (a private class family, a closed interface's slot or field are this module's), so
the test runs after the interface families are collected.
- A type holds a function through an interface when a slot of a vtable its values are made with,
past the header, holds a method or a function-typed field, or a field's type holds one,
recursively; an interface no vtable here makes values with holds one (controller ruling (b):
every interface counting turned the rule off in the ray tracer through `reduce`'s helper, whose
callback parameter takes a `Light`). A class or object cast to `any`, `unknown` or an opaque
pointer takes its fields along (n2b).
- Negatives own_err_declared_callback_indirect, _wrapper, _class_method, _interface_field.
- Spec 31: the C-callback bullet states the test; the CreateTuple/CreateArray retyping under
"Not here"; fix round 4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T6: -mm=own: closed interfaces, re-review 4 - C callbacks by a closed-world surface test
The C-scalar no-drops rule (a body-less declared function of numbers destroys nothing) was
guarded by a list of the routes a function value takes out of the module - a cast, a call into
foreign code - and re-review 4 found two more: a function stored into memory C reads (a
`declare var` of a function type, a `Reference<fn>` a C function returned, an `Opaque` seen as a
class with a function field, a `declare class` with one) and a `@cpp` object handed to C++, whose
C++ vtable slots are this module's methods.
Inverted to a closed-world whitelist (controller ruling, fix round 5): the rule holds only in a
module whose entire foreign surface is scalar - every body-less function (C's, imported, a
`declare class`'s, a `@cpp` slot), every declared or imported global, every `@cpp` class's fields
and bases with no method of its own, every function found in a library at run time - and that
turns no function into data: no cast from a type holding a function to one holding none (a
boolean excepted: it cannot carry one), no cast making an opaque pointer or a reference out of a
number. The call-resolving escape walk and the argument-type tests are removed.
Negatives own_err_declared_callback_stored_global, _stored_reference, _cpp_class (compiled only).
Teeth: with the surface test skipped, own_err_declared_callback prints 77777 9 under own (gc 5 9)
and the new negatives compile. Every probe of rounds 1-4 stays rejected; casts to boolean keep
the rule. Ray tracer as a program: 15 errors (the same). Corpus 528, the same files. Spec 31
rewritten to state the whitelist, with a "Fix round 5" paragraph.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T6: -mm=own: closed interfaces, final patch - fields into every cast, inline_asm and call_intrinsic
Two ways a function became data that the C-scalar whitelist's cast test did not see (re-review
5): a class or array with a function field cast to an integer or a reference
(`<Reference<u64>>k`, then the field loaded as an integer into an all-scalar `signal(s32, u64)`)
- the fields of a class or object input were counted only into any/unknown/opaque outputs - and
`inline_asm<T>(...)` / `call_intrinsic<T>(...)`, which take any operand with no cast.
Now a cast counts the input's fields for every output but a class (its base) or an interface (a
view), and an InlineAsm or CallIntrinsic operand whose type holds a function turns the rule off.
Negatives own_err_declared_callback_field_reference (g2) and _intrinsic (h2), runnable. Teeth,
one switch per fix: each negative prints 77777 9 under own with its fix off while the other stays
rejected. Ray tracer as a program: 15 errors (the same). Corpus 528, the same files. Spec 31: the
three overstated sentences corrected, a "Final patch" paragraph.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T7: -mm=own: a borrowing record's box carries its own release routine; @lends names several
A borrowing record (own spec 32, G2) is an object literal boxed as an interface that holds values
it does not own beside fresh ones. Its box is released through the interface's tag, so the
routine travels with the value: a `ts.NewInterface` marked with the fields the record borrows
(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, set by the signature pass in the next commit) gets a
descriptor of its own whose release routine (`tsrelrec_<type>_<fields>`) frees the box and the
fields it owns and leaves the borrowed ones. Records from different creation sites can then meet
in one local. Precedent: a non-escaping closure's box (`__own_borrows_captures`, descriptor
variant "b").
Stack promotion refuses such a box: its ts.ReleaseContents would give back every field.
`@lends` may name several parameters: a borrowing record holds a borrow of each. MethodInfo keeps
them as a mask (a vector pushed the vtable's SmallVector past its inline-size limit), and the
declaration printer prints them all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T7: -mm=own: result facts are sets, and a returned borrowing record lends its sources
`__own_result_borrows` is now a DenseI32ArrayAttr of argument indices (the receiver is 0 for a
method): one for a plain borrow, any number for a borrowing record, which also carries
`__own_result_record` - the caller owns the box, bounded by those arguments.
own_facts::resultBorrowsAll() reads the set; resultBorrows() stays as the plain-borrow index (-1
for none or for a record) for the callers that need one; borrowedParam() and the plain borrow
take every index. isFresh() is true for a record result, whatever the call goes through.
The signature pass classifies, in each round of the facts fixpoint, every object literal box seen
only as interfaces (classifyRecordBoxes): each field that holds a block is owned (a fresh value,
which moves in) or borrowed (`this`, a parameter, a read out of a place, an owning local's value,
a captured variable's, an immortal), never both, and never a string (strings are copied, spec
22). A box with a borrowed field is marked, with its interfaces, for the lowering and the
inference, and a store into a borrowed field keeps nothing (`keeps`). A function's result is a
record when every heap value it returns is one - a box made here, or a record call's result -
through views, merges and the owning locals that hold it, and every field it borrows is a
parameter's or a read out of one: its facts are those parameters. A record on one path and
anything else on another gives no facts. Families meet by agreement, as before: members whose sets
or record-ness differ are demoted. `@lends` facts are sets; a declaration's `@lends` is a plain
borrow to its callers (a record's box then leaks, nothing is freed twice). No iterator `next()` or
field function whose result is a record is marked fresh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T7: -mm=own: borrowing records, bounded where they are made and where they are returned
The inference half of own spec 32 (G2). A borrowing record's box (marked by the signature pass)
has one owner like any fresh block - the box's own check - and what it borrows bounds it:
- where it is made (classifyRecords, checkBorrowingRecords): every use of the record and of what
is read out of it, through its views, merges and the locals that hold it (walkRecord), is a use
of each borrowed field's source: a read out of a place has them as its own uses (walkBorrowed,
through the field's store, now a borrow), an owning local's value is checked against the local's
ends, a captured variable's against whatever may assign it, a parameter this function keeps
against its moves; `this` and other parameters are the caller's for the whole call. rc's retain
of the record read goes (holdsConstantsOrMoves), its fresh fields moving in as an owning
record's do. Stored anywhere but a local, given to a callee that keeps it, captured, or written
through, it is an error; returned, every field it borrows must be a parameter's that the
function's facts lend.
- where a call returns one (checkRecordResults): the result is fresh, so its moves and releases are
the ordinary ones (G10's edge releases and G7's reads through the new owner compose with it: the
release is the per-site routine). It may hold a borrow of each argument the facts name, so the
chain checkPlaceRead builds for a borrowing call, over all of them, bounds every use of it;
kept, it is `borrows the result of ... and cannot be stored`; returned, the arguments must be
parameters this function lends.
A call's result given a value it records is followed by derivedFrom and by spec 30's taint. A
method reached through an interface is named `<interface>.<method>` in errors
(own_err_interface_result_outlives now reads `the result of 'First.first'`).
Tests: own_borrowing_record (the brief's `closest`, through a closed interface), and
own_borrowing_record_arg (call-argument temporaries with a parameter, a field read, an owning
local, a captured parameter beside fresh values; `const` and `let` locals; a returned record) under
own, rc, gc and none, AOT and JIT, own with no counting; own_lends adds `@lends("a", "b")`.
Negatives own_err_borrowing_record_stored, _outlives, _kept_arg, _let_outlives, _returned_local,
_field_written, _cell, _kept_param. own_err_record_literal_let is a correct program now (the
record borrows the `let`'s value) and is gone; its shape is in own_borrowing_record_arg.
own_err_lends_two_params now reads the body/marker mismatch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T7: borrowing record negatives that read freed memory with their rule off
own_err_borrowing_record_stored now borrows a holder's field that is overwritten before the read,
and _kept_arg a camera that a box's field drops (a stack-promoted local kept its fields alive to
the end of main, so neither showed a freed read). New: _place_overwritten (a field the record
borrows, overwritten before the record is read: the place read's check, with the record's uses
as its own) and _open (an exported function returning a record of its parameter without
`@lends`). Each prints a freed block's value, or crashes, when its rule is switched off (spec 32).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T7: -mm=own: a borrow of a borrowing record is a use of what the record borrows
Found in self-review: walkRecord followed a call's result only when it was a borrowing record that
held the record, so `const h2 = id(h)` (a plain borrow of `h`) was bounded by `h`'s own ends and not
by what `h` borrows: `a = new A()` before `h2.thing.k` compiled and printed 77777 (gc 1). A call's
result that borrows the record, or anything read out of it - a plain borrow, a record that holds it,
a generator's state that borrows it - is now followed, so its uses are the record's.
Negative own_err_borrowing_record_borrowed_again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T7: spec 32, borrowing records
What is accepted (an object literal that borrows `this`, parameters, field reads, a local's or a
captured variable's value beside fresh values; the box owned like any fresh block, released by a
routine of its own that leaves the borrowed fields, and bounded by what it borrows where it is made
and where a call returns it), the facts (`__own_result_borrows` as a set, `__own_result_record`,
`@lends` of several), tests, teeth, measured (own 4.2 MB as rc over 2 million iterations), corpus
(528 before and after, the same files), the ray tracer as a program (15 errors to 10; the rest G5's
and one G8 read), and what is not here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T8: -mm=own: reduce takes its accumulator, and a parameter returned on some paths is kept
G5 of the ray tracer plan. A function that returns a parameter itself on some paths and a value it
owns on the others (`return v.x > 2 ? acc : V.plus(acc, v)`) keeps that parameter (`__own_params`):
its callers move the argument in, the path that returns it moves it out, and every other path
releases it. The signature pass marks such a parameter `__own_params_released`; the inference puts a
release of its slot before each return and each throw that leaves the function, and the move's own
check erases those it reaches and places the release on the edges into the join that did not move it
(spec 30's static placement: no run-time flag). A caller that still uses the argument is "used here
after its value was moved" (own_err_mixed_return, rewritten).
A parameter read into an owning `let` that is given another value is kept too (`let r = init;
... r = f(r, v)` in the generated `___array_reduce`); not a string, which the local copies.
A call through a function-typed parameter of a closed function, which is only ever called and to
which every caller passes a function of this module (its symbol, or a closure over a capture box),
reaches exactly those functions: the parameter's family. The members are closed when their symbols
go nowhere else, and the call gets the facts they agree on, shifted by one for a closure's body
(its box comes first), and `__own_no_drops` when none drops. A member that only borrows a parameter
another member keeps is given it to keep and releases it on every path; any other disagreement
demotes them all ("a function passed in its place disagrees").
own_reduce (the brief's program, an immortal start value returned as it is, early return, return
from a loop and from a `try` with a `finally`, a throw); negatives own_err_mixed_return_join_use,
own_err_callback_escapes, _disagrees, _promoted_used. own_err_borrowed_result_escapes now passes
its function through a `let`: passed straight to `apply`, the call gets its facts and the program is
correct.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T8: spec 33, reduce takes its accumulator
What is accepted (a parameter returned as it is on some paths and an owned value on the others is
kept, its releases placed statically on the paths that do not return it; a parameter read into an
owning `let` that is assigned is kept; a call through a callback parameter of a closed function gets
the facts of the functions passed there, with a closure's box shifted out, and a member that only
borrows a parameter another keeps is given it to keep), tests, teeth, measured (own 4.9 MB as rc
over 2 million iterations), corpus (528 before and after, the same files), the ray tracer as a
program (10 errors to 1: the G8 `getPoint` read), and what is not here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T8: own_reduce runs a promoted callback, called directly too; spec 33 measured
own_reduce passes a named `add`, which only borrows its accumulator, beside `pick` to the one
`___array_reduce<V, V>` (promoted: it releases the accumulator), and calls it directly 20,000
times with a fresh argument (the caller moves it in, `add` frees it once); it prints the same under
own, rc, gc and none. Spec 33 records it, the interface-family probe (two mixed implementations
agree, one beside a plain borrow demotes both), `--verify-ownership` quiet under own and rc, and the
ray tracer run end to end on a scratch copy with `legalize` made a module-level function: checksum
38837369, exit 0, peak 4.9 MB as rc on Windows and 4.3 MB (rc 4.4 MB) on Linux.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T8: say which kept parameters are released where they are not moved
`__own_params_released` names a kept parameter the body never takes into a field, an element, a
global, a keeping call or an assigned owning local; one it does take keeps the old rule (the move
must reach every return). The comment and spec 33 said "on every path", which was not what the code
does. Wording only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T8: -mm=own: a borrow read out of a kept parameter is bounded by its new owner
Found in review: the `let r = p; r = new N(...)` keep rule moved `p` into `r`, and the assignment freed
`p` and `p.v` while `const inner = p.v`, read before the move, was still used (own printed 779000,
gc 501500; BASE rejected the program). The same with a field as the new owner (`h.n = p; h.n = new
N(...)`) was wrong before this task too: a borrow read out of a parameter had no ends, and a kept one
moves away. Now such a borrow is bounded by the new owner from the move on, as a read through the
owner would be (spec 29's destination chain): the owner's ends and the drops of its place after the
move end it; a move into anything else (a keeping call, a return) ends it there, and so do the
parameter slot's releases. Reading it while the new owner still holds it stays accepted.
Negatives own_err_kept_param_let_overwritten and own_err_kept_param_field_overwritten, built through
a @noinline factory (an argument made at the call site can be stack-promoted, and its release frees
nothing). Tooth: with the bound skipped both compile and print 779000 instead of 501500. Spec 33
records it, and in "Not here" the dangling slot after `r = f(r, v)` if `f` throws, and the release
skipped for any thrown value computed from a kept parameter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T8: -mm=own: a call that keeps an argument and is given it again uses it after the move
Found in re-review: `f(p, q)` keeping `p` (spec 33's `let` rule, or a field store) and borrowing `q`,
called as `f(n, n)`, freed `n` under the callee's own borrow (631370 and 639140 where gc prints
501500, and a crash; the field-store form was wrong before this task too). The caller's
use-after-move checks skipped the taking call itself as a use - movesOf for a fresh or folded value,
checkSlotMove's reads for an owning slot. Now (callGivenAgain) a call that keeps an argument is a use
after the move when another of its operands may point into the moved block: the value again, a view
of it, something read out of it, another read of the same slot, its receiver.
Negatives own_err_kept_and_borrowed_arg, _slot, _field (a @noinline factory, churn, an assert).
Tooth: with the check skipped they print 779000 instead of 501500 (two) and crash (one). Spec 33
records it, and the `finally`-after-`return p` narrowing of the previous fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T8: -mm=own: one value at two kept positions of a call is a use after the move
Found in the second re-review: `f(n, n)` with both parameters kept (two `let`s given other values, a
field and a `let`, two fields, two pushes) made two owners of one block. callGivenAgain passed over
every kept operand rooted at the moved value; now it passes over one, the move itself, and a second
is a use after it. The `let` forms were reachable only through spec 33's `let` rule; two fields and
two pushes were wrong at BASE too. Also, for an owning slot, only the reads that reach the call
holding the same value count (`const arr = [n]; n = mk(5); f(n, arr)` is accepted again).
Negatives own_err_kept_twice, _field, _push (a @noinline factory, churn, an assert). Tooth: with every
kept position passed over again they exit 0xC0000374, 0xC0000005, 0xC0000374. Spec 33 updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T8: -mm=own: a slot assigned between a move's read and the call that keeps it is a use after the move
The round-3 read filter in checkSlotMove (a slot read counts for callGivenAgain
only when it reaches the taker without crossing an assignment of the slot)
dropped every read of the old value when the slot was assigned inside the
call's own argument list, the move's read included: `let n = mk(i);
f(n, n, (n = mk(5), 0))` with `p` kept and `q` borrowed was accepted and
crashed (0xC0000005). Underneath, `f(n, (n = mk(7), 0))` with `p` kept is
wrong with no second operand at all: the assignment's release frees the old
`n` before the callee takes it (the `let` form was rejected at BASE, where `p`
was not kept; the field-keep form was wrong at BASE too).
checkSlotMove now rejects a move whose own read does not reach the taker
without crossing an assignment of the slot (`!reachableAfter(load, taker,
kills)`), reporting the assignment between them with the existing text `is
used here after its value was moved` (note: the call, "value moved here").
With the move's read held to that, the round-3 filter is exact: every other
read that passes it holds the moved value. `f(n, (n = mk(i), 0), n)` stays
rejected; `const arr = [n]; n = mk(5); f(n, arr)` stays accepted.
Negatives own_err_kept_arg_assigned_in_call and
own_err_kept_and_borrowed_arg_assigned_in_call (gc and none print 501500; rc
crashes on these shapes, a pre-existing rc codegen issue noted in spec 33
"Not here"). Tooth: with the check skipped both compile and exit 0xC0000005.
own_reduce, the ray tracer (1 error, 279:75) and the corpus (528, the same
files) are unchanged. Spec 33: the read-filter sentence now states what is
checked; "After the third re-review" added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T8: -mm=own: a self-assignment is the move's destination, not an assignment between
The check added by the previous commit (a slot assigned between a move's
read and its taker) used the slot's assignments as kills, and reachableAfter
answers false when the taker is one of them: `n = n` (00owned_locals.ts,
corpus 528 -> 527) was rejected as a use after the move. The taker may be the
slot's own assignment, the move's destination; it is left out of the kills
for this check. `n = n` and `n = (n = mk(5), n)` print as under gc; the
probes, the negatives, own_reduce and the ray tracer (1 error) are unchanged;
corpus 528 again, the same files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T8: -mm=own: a slot assigned on some path between a move's read and its taker is a use after the move
The check from round 4 (`!reachableAfter(load, taker, kills)`) asked whether
every path from the move's read to the call that keeps it is assigned. With
the assignment on one arm of a conditional in the argument list -
`f(n, i % 2 == 0 ? (n = mk(7), 0) : 0)`, the same through && or ||, or with
an await on the other arm - the other arm is a clean path, the move was
accepted, and on the assigning arm the old n was freed before f kept it
(exit 139; BASE rejected the let forms; the field-keep form was wrong at
BASE too).
The test is now existential: the move is rejected when some assignment of
the slot other than the taker lies on a path from the read to the taker
(`reachableAfter(load, a, varOp) && reachableAfter(a, taker, {load, varOp})`),
reported at that assignment with the existing `is used here after its value
was moved`; the all-paths test stays as a fallback, and a read and taker that
reachableAfter cannot order (orderedInBody) refuse instead of passing. The
stale reachableAfter comment (async bodies and lambdas are functions of their
own by this pass) is corrected.
Negatives own_err_kept_arg_assigned_on_arm (c1a) and
own_err_kept_arg_assigned_in_and (c2); gc and none print 501500, rc crashes
(pre-existing, spec 33 "Not here"). Tooth: with the check skipped, both and
the field-keep form compile and exit 0xC0000005. Still accepted and equal to
gc: o12/o12b, ok1-ok5, a16d, a16fd, o13/o13r/o13p, own_reduce, l5g, sa, sa1,
sa1b, sa4, sa6-sa8; 00owned_locals clean; ray tracer 1 error (279:75);
corpus 528, the same files. Spec 33: the two false sentences corrected,
"After the fourth re-review" added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace merge: a global initializer's call borrows when its facts name any argument
Main's ownGlobalInitializerCalls (#638) tested Facts::borrows >= 0, a
single index on main; this branch made the result facts a set (T7, a
borrowing record lends several sources), so the merged tree did not
compile. Any named argument is a borrow; a borrowing record always
names its sources, so it is covered too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace merge: a global may be given an immortal global's value
Main's rejectBorrowedGlobalInitializers (#636) rejects a global
initialized with another global's value: under own it would be a second
owner. An immortal global's value (spec 25, T1) is owned by no one -
its block is marked and no release frees it - so `let e = c` with `c`
immortal is accepted again, as own_immortal_global_region expects
(own, rc, gc, none; no counting). own_err_global_from_global still
fails: its source global is reassigned, so it is not immortal, and a
field read (own_err_global_from_field) never is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace merge: negatives that main's own rules now reject first
Main's own checks run in OwnershipSignaturePass before this branch's
rules can report, and reject the same programs:
- own_err_immortal_region_call / _borrow (T1, spec 25): a global region
holding a call's borrowing result is rejected by #638
(ownGlobalInitializerCalls) first. Same programs; the regexes now pin
#638's message. findImmortalGlobals still runs first and still leaves
those globals mortal.
- own_err_read_after_move_global / own_err_field_let_global_overwritten
(T5, spec 29): `run(G)` with `run` assigning G is rejected at the call
by #639 (rejectGlobalArgumentsDropped). The rule they pin is the
callee's, so main() now gives each callee a fresh value
(run(mkH()), new K().run(), runCallee(mkH())); every original message
still fires, at lines 42/58/66 and 42.
- own_err_param_kept_not_owned (phase 4): `keep` stored into h.c, which
may drop, so #639 fired first; it now pushes into an array, which
frees nothing, and phase 4's "is given to 'keep', which keeps it"
fires again.
Spec 25 and 29 note the merge; 29's "still open" `run(G)` reading a
parameter is closed under own by #639.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace merge: an immortal static may be lent to a call that may drop
Main's rejectGlobalArgumentsDropped (#639) rejects a value read out of a
global something writes, lent to a call that may drop. writtenGlobals
holds an immortal static's one store in its static constructor (spec
25), so `lend(b, K.w)` - lend freeing b's old field - was rejected
after the merge, although no release frees K.w's block and nothing
assigns it again. An immortal global's read now steps aside, as it
does in #636's check. A module-level immortal global was not affected
(its initializer is its region, not a store).
own_immortal_global gains the call (own, rc, gc, none; no counting);
spec 25 notes both of main's global checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T9: a direct call's result is not a use of a borrow given to it
The ray tracer's last error under -mm=own (G8): render's
`getPoint(x, y, scene.camera)` makes a fresh vector, but walkBorrowed took
every call result as pointing into the call's arguments, so the vector's
later uses were uses of the borrow `scene.camera`, and
`Color.toDrawingColor` (which builds and calls a closure, so may drop)
ended that borrow first.
A direct call of a function this module defines (isFresh) now ends the
walk at the call, as a call with `__own_fresh_result` already did: its
result points into nothing it was given but what its facts name (a
borrow of an argument, a borrowing record, a generator's state that
borrows its arguments - each still walked), and a body whose callers
cannot know such a fact is an error.
Tests: own_fresh_call_result (own_test + rc/gc/none); own_raytrace, the
own copy of the benchmark at 64x64 (checksum 606318 under gc, none, rc
and own; under own through the JIT entry and `--emit=exe`,
own-exe-run.cmake, since its exports open its interfaces in an object
file); negatives own_err_call_result_bounded / _record. Tooth: with the
gate dropped the bounded negative compiles and crashes (139).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T9: the ray tracer benchmark under own
rtbench_own.ts is rtbench.ts with the plan's two source edits (the
surfaces are Shared<Surface> handles; Plane.normal returns a fresh
vector), at 512x512; bench.ps1 builds it for -mm=own, with and without
stack promotion. The "does not compile" rows of both READMEs become
628 ms / 4.5 MB (627 ms without stack promotion; rc 638 ms / 4.5 MB in
the same session, Windows, median of 7, two runs of bench.ps1). Spec
section 34 records G8's cause and fix, the tests, the tooth, the
measurements and the corpus (525 -> 526).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T1: a global's one store that may run again leaves it mortal
findImmortalGlobals accepted a global whose single initializing store
sits in a loop - of a static block, or of top-level script code, which
is a function a ts.GlobalConstructor runs. "Written once" counted
store ops, not how often they run: every block the store was given
got the immortal header, and the ReleaseSlot before each store skipped
the old one. A million V's of 64 numbers peaked at 589-602 MB under
own, 4.4 MB under rc. A leak, not a use-after-free.
The store must now run once (mayRunAgain: no enclosing loop op, and
its block on no cycle of its region's CFG - after the affine lowering
every loop is such a cycle). Such a global is mortal: each store
releases the old value, and the probes peak at 4.4 MB. A store in an
`if` arm is still accepted.
own_global_loop_store (static block loop, top-level while, `if` arm)
runs under own/rc/gc/none; test-own-global-loop-store-mortal checks
only the `if` arm's global is marked. own_err_global_loop_store_static
and _script pin that the loop-written global is mortal: they compiled
before. Spec 25: the top-level script sentence corrected, and a
fix-up note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace merge: negatives for #639's shapes and the region-call refusal
d691710e gave the callee-side negatives fresh values, so the original
programs' shapes were pinned by no negative. Each is now a #639
negative of its own ("a value read out of the global '...' is lent to
a call that may release or overwrite it"):
- own_err_global_argument_transitive: run(G), run calls reset, which
assigns G
- own_err_global_method_transitive: G.m(), m calls reset
- own_err_global_this_field_move: GK.run() moves into this.f, then
assigns GK
- own_err_global_argument_field_let: run(G), const x = h.c, then G
assigned
- own_err_global_kept_field: keep(h, g) stores into h.c, g assigned
by a reset elsewhere
own_err_immortal_region_borrow now calls through a function value
(`const c = pick()(list)`), whose callee #638 cannot know, so it again
shows that findImmortalGlobals keeps a call's result mortal: "borrows
the global 'c' and cannot be stored, returned or captured".
own_err_immortal_region_call keeps #638's message. Spec 25 and 29
notes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T9: one helper for a call whose result is the caller's own
walkBorrowed's two clauses that end a walk at a call - the
__own_fresh_result one and T9's direct call of a defined function -
are one helper, callResultIsOwn, so the two exemptions read as one
rule. No change in behaviour.
own-exe-run.cmake matches EXPECT as a whole line and "done." as the
last line (`checksum: 606318` no longer matches `checksum: 6063180`).
Spec 34: a string read out of a parameter and returned is a borrow
fact like any other (`nm(c) { return c.name; }` gets
__own_result_borrows), not a copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace T1: a read of a mortal global ends at its assignment
Making loop-written globals mortal exposed an older hole (on main too):
`const c = G` names the global's read itself - a ts.Load of its
ts.AddressOf with no retain - and no check bounded it. `let c = G` is a
borrowing local, rejected; the const was neither a place read nor a
local. So `const c = K.w; K.w = new V(i + 1); print(c.x)` in a static
block's or top-level loop compiled and read the freed block (exit 139
on Windows and Linux), and so did the same in any function.
OwnershipInferencePass checks each read of a mortal global
(isGlobalRead / checkGlobalRead): no use of it, nor of what it produces
(walkBorrowed, made again at the read), may come after a ts.Store or
ts.ReleaseSlot of that global in the same function.
Where that walk meets a keep, the read is followed into what holds it
or rejected, never let go:
- followed (borrowHolderOf): a local declared from it, assigned it, or
given it in a field of its value tuple - an object literal built in
place, a `let` taken out of one, a destructuring. Its uses under the
field path that holds the borrow are uses of the read (a sibling
field is not), made again only where the holder is given a new value
(holderRenewals: its declaration when it has an initializer, a store
into that path or a prefix of it) - never at the read, which renews
`c` and not `o.v`: a holder declared before the loop keeps the old
borrow across the back edge (`const o = { v: Z.z }; for (...) {
const c = G; print(o.v.x); o.v = c; G = new V(i + 1); }` read the
freed block), and so does `let l!: V` declared inside the loop, whose
slot stores nothing and keeps the last round's value (walkBorrowed's
renewal of a non-owning local at its declaration says the same now).
A value loaded out of it is made again only by a path back through
that load; a
ts.DeconstructTuple is followed field by field; a value made from the
borrow that declares or is stored into another local makes that a
holder too;
- rejected: any other keep ("the value read from 'G' is kept where its
end at an assignment of 'G' cannot be checked"). A string read is
copied where it is kept (spec 22).
A call that assigns the global stays #639's at its arguments.
Negatives own_err_global_const_store_static, _script,
own_err_global_const_store, own_err_global_record_static,
own_err_global_record, own_err_global_let_holder_static,
own_err_global_let_destructure, own_err_global_let_holder,
own_err_global_outer_holder, own_err_global_outer_holder_static,
own_err_global_uninit_let and own_err_global_uninit_let_static; with
each rule skipped, its negatives compile and exit 139 under
MALLOC_PERTURB_ on Linux. Corpus unchanged (526); the ray tracer has no
error.
Also: own_err_immortal_region_borrow's firstOf says `@lends`, so the
file has no error but the refusal it pins; own-exe-run.cmake's header
rewrapped; spec 25 states the rule, the message as printed, and what is
not closed (a call that assigns the global, `g(n, n = mk(2))` with a
local); spec 34 records the limit of the T9 teeth.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* own-raytrace final: write-guard negatives, shared helpers, spec fixes
The whole-branch review's fix list (final-review.md I2-I4, M3, M4, M6,
M7). No change to what -mm=own accepts, except a method's @lends past
argument 63, now an error under every memory model (MLIRGen).
I4 - the immortal-global write guards each get a program and an IR
check (test-own-global-mortal-*, --emit=mlir-affine: the guarded
global's ts.AddressOf is not marked __own_immortal, the control's is):
an exported `let` and an exported class's static, a static written
through `k: typeof M` (the vtableWritten route), two stores in one
static block, and `this.w =` in a static method as the static's only
store. Teeth: one OWN_TEETH switch per guard (removed) fails exactly
its own check.
I2 (behaviour-preserving):
- resolveInterfaceSlot / resolveInterfaceField were one body over two
maps: one template taking the family map.
- "the initializer region yields only the default" is one helper,
own_facts::regionYieldsDefault, used by findImmortalGlobals and by
GlobalOpLowering; they can no longer disagree on the op list.
- own_facts::globalReadOf is the load-of-AddressOf walk that
isImmortalGlobalRead and findImmortalGlobals' readsImmortal share
(the latter still tests the set being found, not the attribute).
M4 - a method's @lends is kept as a 64-bit mask for the class's
declaration; an index past 63 was dropped silently, a borrow an
importer never learned of. Now an error, own_err_lends_index_64.
Spec (own-model): the §21.2 row no longer names the deleted
own_err_record_literal_let and says what §32 accepts; §25's holder
renewal says "when it has an initializer" as N4 does; §33's rc note
says "a call that keeps it"; §31 says the exe rule does not cover
functions (callableFromOutside: a sound over-rejection); §34 loses its
process note; §25 and §32 record the new tests and teeth. Design doc
G1 no longer lists a fresh call result. Both READMEs date the own row
2026-10-23 in the table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 23, 2026
ASDAlexander77
added a commit
that referenced
this pull request
Oct 23, 2026
…#661) * -mm=own: a read of a global ends at a call that may assign the global `const c = G; reset(); print(c.x)`, where `reset` does `G = new V(1)`: the read borrows what `G` holds, and the call frees it while `c` still reads it. own accepted it and read freed memory (exit 139), also when the assignment is a call further down (`step()` calling `reset()`). checkGlobalRead (#655) ended such a read only at a store to the global in the same function; follow-up 11. OwnershipSignaturePass pins on each call the globals it may give a new value (pinAssignedGlobals): a store to the global, or its release, in a callee or in a call it makes, to a fixpoint; a call that may drop and whose callees are not known may assign any, one that drops nothing assigns none. checkGlobalRead counts such a call among the read's ends. A read handed to that call as an argument stays #639's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Update CMakeLists.txt --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
function run(h: C) { G = mk(); churn(); print(h.x); } run(G);read freed memory under rc and own. So didG.m()wheremassignsG, and under rcrun(G.c)whererunoverwritesG.c. An argument is borrowed, and the callee doesn't know its parameter is the global's, so the global's release freed the block the parameter still read.rc:
holdGlobalArguments(OwnedReturnConsumptionPass, rc only) has the caller hold a count across the call: a retain right before it and a release right after. It applies to an argument read out of a global, or out of a field or an element of one, including a method's object (G.m()). Two cases are left as they were:ts.Invokeends its block. That case stays open.Shared<T>handle, whose count a program reads withShared.count. Holding a count there changedown_shared_global*'s count checks.own:
rejectGlobalArgumentsDropped(OwnershipSignaturePass, a new helper and one call aftercomputeDrops) makes it an error to give a global's value to a call that may drop (no__own_no_drops), when anything in the module assigns that global. own already rejected a field of a global given that way. The new rule rejects none of the existing own positives (full suite green).Tests
tests/00global_argument_held.ts(default + rc/none corpus): global, field of a global, method of a global.own_err_global_argument_assigned(the probeg6),own_err_global_method_assigns.Results
ctest -C Release, 4864/4864 passed🤖 Generated with Claude Code