Repository navigation
own: compile the ray tracer under -mm=own - #655
Merged
Merged
Conversation
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>
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>
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>
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>
…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>
…resh 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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot 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>
…ot 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>
…e 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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
…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>
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>
…ed 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>
… 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>
…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>
…n 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>
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>
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>
…ted 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>
…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>
…es, 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>
… 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>
…s 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>
…s 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>
…o 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>
…er 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>
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>
… 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>
…e 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>
…unded 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>
…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>
…s 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>
…he 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>
…, 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>
…'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>
Brings in the ray tracer benchmark (#634: tslang/benchmarks/raytrace, README Performance) and main's rc/own fixes #635-#645 (rc counts for const reads of fields/elements, globals copied from globals or fields, Shared/global lent to a call that assigns it, kept bound methods, interface/bound-method views of locals, own facts in a global's initializer #638, own rejects a global lent to a call that assigns it #639). Conflicts (1): - tslang/test/tester/CMakeLists.txt, own_error_cases: this branch added the own_err_immortal_* negatives (T1, spec 25) where main added own_err_global_argument_assigned / own_err_global_method_assigns (#639). Both lists kept, in that order. Auto-merged without conflict: Defines.h, LowerToLLVM.cpp, MLIRGenImpl.h, OwnershipSignaturePass.cpp. Main did not touch the own spec, so no section renumbering (this branch's sections end at 33). The merged tree does not compile as is: main's ownGlobalInitializerCalls (#638) reads Facts::borrows as one index, which this branch made a set (T7); fixed in the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s 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>
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>
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>
… 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>
…n 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>
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>
…rtal 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>
…efusal d691710 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>
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>
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>
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>
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.
Summary
-mm=own(single-owner memory model) now compiles the ray tracer benchmark (tslang/benchmarks/raytrace/rtbench.ts). The own copy differs from the source by exactly two edits: the surfaces becomeShared<Surface>, andPlane.normalreturns a newVector. Everything else is fixed in the compiler.own_raytracetest, JIT and an AOT exe).What changed (own-model spec §25-§34)
Sharedliterals; object literals that own their fields.-sharedbuilds stay open).reducetakes its accumulator; a parameter returned on some paths and fresh on others is kept; callback families.checkGlobalRead): a read of a global that can be freed is a borrow ending at the global's next store in the same function. Every holder of that read (a local, a const or let, an object literal at any depth, a destructured local) is followed or rejected. This also closes use-after-free shapes that own accepts on main today, for exampleconst c = G; G = new V(); print(c.x).Tests
own_*positives andown_err_*negatives. Each soundness rule has a tooth (with the rule switched off, its negative compiles and crashes).MALLOC_PERTURB_/MALLOC_CHECK_on Linux.Not in this PR
Pre-existing bugs that main also has: an object literal re-viewed as another interface, a slot assigned in a call's argument list under rc, a borrow of an owning local freed during the borrow (
g(n, n = mk(2))), interface-typed global copies under rc, and a callee assigning a global after a const read. Also, the two ownership pass files are large and should be split. Each is proposed as follow-up work.Main's #646-#648 (fuzzer tool, rc global-argument holds) are not merged into this branch yet. A trial merge has no conflicts.
🤖 Generated with Claude Code