The Jac No-GC Profile: headerless correctness, receiver modes, container moves, borrow returns, views by inference, lin, and the [memory] flag clean break (#8984) - #8986
Merged
Conversation
… in the callee, take()/swap() move out of places Under --gc none a named `own` local stored into an own field, an optional own field, or an inline-flattened field left its binding live, so the last-use drop freed the value the field now owned (jaseci-labs#8984 A1, A1b). Own-typed optional fields were never torn down by the parent's drop, own parameters were never dropped by the callee, and the structural loop-move rule in the checker rejected a binding that the dataflow could see was revived before the back-edge. - OwnershipCheckPass stamps `Module.own_move_sites`: the Name uses at which a verified-owned binding is consumed. The native backend consults it and `rc_move_in` through one primitive, `_nogc_consume_local`, which nulls the source slot at a move; the three inline copies in container element prep collapse onto it and field stores now use it. - `__drop_<T>` tears down optional own fields; scope, loop-iteration, and last-use cleanups release optional slots in every gc mode; overwriting an own field drops the old value; own parameters are owned by the callee under headerless codegen and get last-use facts from RcFactsPass. - `take(place)` and `swap(&mut a, &mut b)` builtins: type stubs, Python runtime helpers with compiler lowering for names, attributes and items, native lowering through a shared `_codegen_place_ptr`. E1316 rejects moving a heap value out of an owned slot into an owning destination without take(). - The structural loop-move rule and `_loop_depth` are removed; the CFG dataflow already proves the moved-in-a-previous-iteration case. - Under the dev-source reroute the stub catalog locator validates a shipped catalog's key against the checkout, so a `.pyi` edit rebuilds the dev catalog instead of silently typing new builtins as Unknown. - tests/support gains native_build/native_run/python_run/assert_native_identity and the nogc_identity fixtures pin headerless == rc == cycles == python.
…/insert/del/extend rebox or memmove by value; E1317 for index moves `list[own T]` selected the inline (AoS) element layout from the annotation alone, which the type system erases, so any seam that paired `list[own T]` with `list[T]` (a `&list[T]` parameter, a generic instantiation, a borrow return) ran pointer-layout accessors over a by-value buffer (jaseci-labs#8984 A4, A5), and a base-typed list stored subclass payloads truncated to the base (jaseci-labs#8984 A3). `pop()` on an AoS list handed out an interior pointer that the receiver later freed (jaseci-labs#8984 A2). - The layout is decided once, in `_lower_class_type`, from the element's semantic type: inline only for a closed-world leaf archetype (no subclass anywhere in the program) with no heap slots, in a nogc-enforced module. Type variables resolve through the monomorphization solution, so a generic body and its caller agree. The syntactic `list[own T]` shortcut is gone. - AoS `pop` reboxes the element into a fresh shell before the tail moves; `insert`, `del xs[i]`, and `extend` move payloads with memmove/push; `remove` (identity equality) is not lowered for by-value elements. - Every new static drop (optional own fields, optional slots at scope, loop and last-use cleanup, own parameters in the callee, drop-on-field-overwrite) applies only in the owned world: a nogc-enforced, non-kernel module. `CompileOptions.nogc_enforced_for` is the single predicate (it now covers the CLI target) and the checker delegates to it. - Constructor arguments consume their move-site source, so an `own` parameter stored into a field is not dropped again by the callee. - E1317 rejects moving a heap element out of an owned list into an owning destination; `pop()` and borrows are the two ways out. - The nogc_identity suite grows an owned-list operations fixture and the generic fixture returns a borrow; 9/9 identity tests pass headerless.
…d written as a typed self A method call was invisible to borrow discipline: nothing said whether a method read, wrote, or consumed its receiver, so `c.inc()` while a shared borrow of `c` was live compiled clean (jaseci-labs#8984 B2, rule 2). - OwnershipCheckPass infers a receiver mode per method: `&mut self` when the body assigns a field of self, grows or mutates a container field, takes `&mut self`, or calls another mutating method (fixpoint over self-calls, cycles resolve to the least mode); `&self` otherwise. The mode is stamped as `Ability.receiver_mode`, the fact an editor hint reads. - The explicit form reuses the typed-self parameter the grammar already parses: `self: own T` consumes the receiver, `self: &mut T` and `self: &T` pin the mode. E2015 now admits an explicit self in obj/node/edge/walker methods only when it carries a mode, and says so. - A mutating call is a write: E1303 while a shared borrow is live, E1309 on an `imm` binding, and E1318 when reached through a shared borrow. A `self: own` call consumes the receiver in the dataflow (E1301 on a later read) and is a move site the native backend consumes; the callee owns and drops it under the owned world. - The Python backend no longer emits a duplicate `self` when a method declares its receiver explicitly. - Eight receiver-mode cases join the ownership code table and an identity fixture pins the own-self drop across backends.
…f optional slots only in the owned world
…d containers of borrows are stack-scoped, second-class values A type that transitively holds a borrow is a view (jaseci-labs#8984 rule 5, B1): an archetype with a `&`-typed `has` field, a binding declared over a borrow (`list[&Item]`), or a value built from either. The checker now treats such bindings as borrows of everything they were built from, so the existing E1302/E1303/E1304/E1306/E1308 rules police them: a view returned over a local is E1306, stored into a field or global is E1306/E1315, sent across `flow` is E1308, and mutating an owner while a view of it is live is E1303. A view derived from the function's borrowed parameters may be returned, and a return with several borrowed candidates ties the result to all of them (rule 4) instead of being rejected. - BorrowMeta carries extra owners; writes and moves are checked against every owner of a view. View bindings created from constructors reach in the borrow dataflow like `&x` bindings do. - The native backend never drops through a borrow field, lowers a declared `list[&T]` with the pointer layout regardless of the element's owned layout, and stamps no element drops on a view container. - Drop hooks are resolved through a forward declaration, so a list of hooked elements referenced from a struct field before the method is declared still runs the hook when the list drops. - E1315 is the general view code; its text names the three view forms. - Eight view cases join the ownership code table, a view fixture joins the identity suite, and the reference documents take/swap, receiver modes, the owned-list layout rule, and the new codes.
…er modes of foreign methods come from the stamped fact, never from walking another module's tree
…only when it carries type facts A cold compiler shares the self-host program's module trees with the program being analyzed. A tree the self-host built for bytecode alone has no TypeOf facts, so a base class resolved through it was a hollow class and every inherited method vanished (jac check on scale/plugin.jac after a compiler edit). The analysis ledger already says whether a tree is typed; load_dependency_module now consults it before adopting a foreign tree and otherwise compiles its own analysis view, counting the skip as a cache event.
…ing, inlay hints for inferred ownership Rule 7: `lin` is a keyword. A `lin` binding is an `own` binding in every respect (moves, borrows, drops, codegen) plus a must-consume check: a forward must-analysis over the CFG (`solve_forward_must`) reports E1305 on any path where the binding reaches the end of its scope unconsumed. `SubTag.linear` and `Symbol.linear` carry the marker; the compiler's own C3 code no longer uses `lin` as a name. Rule 3 (containers): a named `own` archetype binding appended or inserted into a list, stored as a dict value, or placed in a list literal is a move. The checker treats grow-method arguments of archetype type as consuming, the E1406 fence admits fresh and moved archetype elements for lists and dict values (sets and borrowed values stay fenced), dict overwrite and `del` drop the old value through the element drop slot under headerless codegen, inline-layout list literals use the inline helper set, and rc and cycles transfer ownership at the call the way nogc does (`NaIRGenCalls._transfer_own_args`, managed branch of `_nogc_elem_prep`), so every profile prints the same drop order. Rule 6: under enforcement `raise`, `try` and `except` lower to a hidden error slot instead of setjmp/longjmp. Every ability carries an inferred raises effect (`Ability.raises`, fixpoint in the ownership checker); a raise stores into shared `__jac_err_*` globals and propagates; each call to a raising function is followed by a check that dispatches to the enclosing `except`, runs `finally`, or drops the frame's owned locals and returns; an entry block that does not handle a raising call is E1407 at compile time. Rule 9: `textDocument/inlayHint` in the language server, backed by one `inferred_hints` producer in the compiler (ownership states of unmarked locals, view bindings, receiver modes, raises effects). Dev loop: under the dev-source reroute the native compiler kernel lookup stops at the checkout (the shipped kernel was still lexing and parsing); dependency adoption from another program exempts stub trees (the previous rule re-parsed typeshed unboundedly during a catalog build) and never replaces the foreign program's live tree. Part of jaseci-labs#8984.
… --native, jac explain, and no compile-time JAC_* variables Rule 10 of jaseci-labs#8984 as a hard clean break. One declaration of memory intent: `[memory] profile = managed | rc | nogc` with `enforce` / `exempt` for the incremental case, replacing `[gc] default` and `[gc.enforce]`; `[native] target / opt / debug / threads` replaces `[build.native]`; `[placement] default` replaces `[build] default_codespace`. Retired sections are reported with their new home; both config loaders share one parser (`_apply_memory_native_placement`, `_apply_build_section`). `jac nacompile` is gone (tombstoned with the new spelling). `jac build <file> --native` compiles one self-contained artifact, a binary when the module has `with entry` and a C-ABI library otherwise (`--lib` forces it), with `--memory`, `--target-triple` and `--debug` as its only knobs; `native` leaves the `--as` list. The former flags became invariants: the RC-free IR scan always runs under `nogc`, a demoted function always fails a native artifact, and `--scrub` is `jac clean --cache`. `CompileOptions` speaks `memory_profile` and derives the runtime mode; no compile-time environment variable sets behavior (the target override is a context variable beside the codespace override). A built binary reads only `JAC_GC=off` and `JAC_THREADS`, and the cycle collector runs by default under `managed`. `jac explain memory | placement | ir` replaces the diagnostic variables and `jac check --placements`, sharing the `inferred_hints` producer with the language server. Turning the collector on by default exposed two runtime faults that the identity and differential tests caught: a release that drops a buffered possible root to zero skipped its destructor (now it empties the roots slot, clears the buffered bit, and reclaims at once, and the collector's root passes skip emptied slots), and the driver's option carry-over must copy only enforcement facts, never the runtime mode. Tests, docs, example scripts and the desktop build paths are migrated; `tests/support.native_memory_flags` is the single place the test flags live. Part of jaseci-labs#8984.
marsninja
force-pushed
the
nogc-profile
branch
from
September 5, 2026 22:03
1be9c75 to
d8479bf
Compare
… and E1406 says what to write Rule 1 of the No-GC Profile (jaseci-labs#8984): in an enforced module an unmarked local takes its state from its right-hand side. A call, container literal, f-string, concatenation or `own` copy is `own`; a string literal is `imm` (or `own` once rebound); a field or element read borrows its root, mutably when the root allows it and the local is written through; `&place` and reborrows tie to their owner; a conditional whose arms agree takes their state, and one whose arms disagree stays E1401. Contract positions stay explicit in every module. Rule 9: E1406 names the value, why it cannot enter the owned world, the destination, and the idiom that fits the site (`out.append(own p)`, `take(place)`, iterate by value, or a fresh value); `own <expr>` on a str is the accepted copy-in idiom. The builtin container mutators join the receiver discipline as `&mut self` methods, so a shared borrow can no longer grow a container (E1318). The container fence treats the type system's `LiteralString` as str, so `p.upper()` on a literal-narrowed receiver no longer trips it. Native backend: a borrow local is exempt from headerless scope-exit and overwrite drops, and the owned-list layout is decided once from the element type regardless of emission order (an entry-block global typed before the element archetype's body was registered used to get the inline layout while every later use chose pointers). Build: the launcher stub builds through `jac build --native`, and a `--lib` build no longer refuses Python-only demotions in its closure.
…memory takes --memory The clean break removed compile-time reads of JAC_NATIVE_TARGET, so the cross-target tests (wasm32, aarch64, windows, darwin) now set the target through push_native_target / pop_native_target from the compile-options module, and the two aarch64 subprocess tests pass --target-triple. jac build --native --target-triple publishes the triple to every compile-time consumer for the duration of the compile, the way the wasm builder already did. jac explain memory <file> --memory managed|rc|nogc explains a module under a profile other than the project's and prints the RC coverage line on stderr; the stack-promotion test reads promoted=N from it instead of the retired JAC_RC_STATS variable. The wasm RC audit test probes for JAC_GC, the one variable a built binary still reads.
# Conflicts: # jac/jaclang/compiler/backends/native/na_ir_gen/calls.jac # jac/jaclang/compiler/backends/native/na_ir_gen/container_helpers.jac # jac/jaclang/compiler/backends/native/na_ir_gen/exceptions.jac # jac/jaclang/compiler/backends/native/na_ir_gen/func.jac # jac/jaclang/compiler/backends/native/na_ir_gen/lists.jac # jac/jaclang/compiler/backends/native/na_ir_gen/objects.jac # jac/jaclang/compiler/backends/native/na_ir_gen/refcount.jac # jac/jaclang/compiler/backends/native/na_ir_gen/state.jac # jac/jaclang/compiler/backends/native/na_ir_gen/types.jac # jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/calls.impl.jac # jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/container_helpers.impl.jac # jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/exceptions.impl.jac # jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/func.impl.jac # jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/lists.impl.jac # jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/objects.impl.jac # jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/refcount.impl.jac # jac/jaclang/compiler/backends/native/na_ir_gen_pass.impl/types.impl.jac # jac/jaclang/compiler/backends/py/impl/jcir_gen_pass.impl.jac # jac/jaclang/compiler/backends/py/jcir_gen_pass.jac
marsninja
marked this pull request as ready for review
September 6, 2026 19:09
Contributor
|
Too many files changed for review (376 files, 100 file limit). Bypass the limit by tagging |
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.
Executes the No-GC Profile RFC (#8984) end to end, as one PR by request.
What lands
ownlocals into optional, inline and constructor-argument places;list[own T].pop(); subclasses inlist[own Base]; borrow returns of list elements; generics over borrowed lists. Each shape is pinned by an identity test (tests/compiler/backends/native/test_nogc_identity.jac): nogc, rc, managed andjac runmust print the same text.append,insert, dict stores, list literals take namedownarchetypes; dict overwrite anddeldrop the old value;pophands the element out), plustake(place)andswap(&mut a, &mut b)builtins on every backend (E1316, E1317).def m(self: own T); E1303/E1309/E1318 for mutation through the wrong receiver.finally, and E1407 for an entry block that does not handle a raising call.lin(must-consume) with E1305 from a must-analysis over the CFG.own, string literalsimm, field and element reads borrow their root with the root's mutability decided by use,&placeand reborrows tie to their owner, agreeing conditional arms take their state); contract positions stay explicit in every module.LiteralStringresults (p.upper()on a literal-narrowed receiver) count as str for the container fence.jac explain memory | placement | ir;E1406now names the value, why it cannot enter the owned world, the destination, and the idiom that fits (out.append(own p),take(place), iterate by value, or a fresh value). Builtin container mutators (append,insert, ...) are&mut selfmethods under the receiver discipline, so a shared borrow can no longer grow a container (E1318).[memory] profile = managed | rc | nogc(+enforce/exempt),[native] target / opt / debug / threads,[placement] default;jac build <file> --native [--lib] [--memory] [--target-triple] [--debug]replacesjac nacompile;--assert-no-rcand--strictare invariants;--scrubisjac clean --cache; no compile-timeJAC_*variable sets behavior; a built binary reads onlyJAC_GC=offandJAC_THREADS; the cycle collector runs by default undermanaged.Build fixes found on the way
nacompileverb; it builds throughjac build --nativenow.--libnative build (the payload's compiler kernel) no longer refuses Python-only demotions in its closure: a shared library runs those in Python, and only a self-contained executable cannot.Native codegen fixes found on the way
&place) is exempt from headerless scope-exit and overwrite drops; before,it = xs[0]; it.name = ...destroyed the element.Runtime fix found on the way
Dev-loop fixes found on the way
.pyiedits take effect.Deviations from the RFC text
--as clientstays (the RFC listsclientas leaving the--aslist; it has no replacement spelled out, so it is out of scope here).examples/jaclang_org/jac.tomlmoves from[gc] default = "none"plus enforced modules to[memory] profile = "managed"with the same modules underenforce: a headerless build without enforcement is the unsound corner the RFC retires.JAC_NA_DEBUGremains as a developer switch: it only prints, andjac explain placementis the user-facing surface.Verification
Targeted suites (identity, ownership code table, container fences, local inference, raises, abort drops, ownbench differential, compile options, verb surface, docs parity, language server) pass locally; CI covers breadth.
tests/support.native_memory_flagsis the single place the test flags live.