Tnode.PlacementsThe context-scoped side of the memory-mode split (docs/proposals/context-scoped-memory-modes.md): memory_mode on the tnode is *declared intent* -- requests made at graph-construction time (user set_materialized, parameter/constant marking, op-support Never_virtual) -- while the *decisions* (resolving to Virtual / Local / On_device) are recorded here, per compilation lineage. A Placements table rides Low_level.optimize_ctx: it is copied at the start of each backend compile, so sibling compiles from the same context are hermetic (a candidate compile cannot poison another's placement resolution), while child contexts inherit the lineage's decisions.
Lookups fall back to the tnode's intent when the lineage has not yet decided; updates apply the same lattice as update_memory_mode but never write the tnode. Intent strengthened after a lineage compiled does not invalidate that lineage.
module Key : sig ... endval sexp_of_t : t -> Sexplib0.Sexp.tval create : unit -> tval get : t -> t Base.Hashtbl.key -> (memory_mode * Base.int) Base.optionThe effective placement state: the lineage's decision if any, otherwise the tnode's declared intent. Side-effect free.
val update : t -> t Base.Hashtbl.key -> memory_mode -> Base.Int.t -> unitKernel-fission escape hatch: strengthen a routine-scoped Local decision to On_device. Local is only ever a compiler decision (never declared intent), premised on the node's lifetime being confined to one kernel launch; when the schedule layer splits a routine into multiple kernels at a point where the node's live range crosses, that premise is withdrawn and the node must own a context buffer. The update lattice deliberately rejects Local -> On_device (decisions are final within a lineage); this is the one sanctioned override, sound exactly because fission runs between optimization and code generation — before any consumer of the decision (codegen parameter lists, context allocation) has read it.
val raw_entry : t -> t Base.Hashtbl.key -> (memory_mode * Base.int) optionFission bookkeeping around promote_local_to_device: the raw lineage entry (no intent fallback), captured before a promotion so unsafe_restore can undo it when segment coalescing removes the crossing that motivated it. Undoing is sound in that direction only: schedules computed under the (stricter) materialized view remain valid for a Local node, while the converse would miss coverage requirements.
val unsafe_restore :
t ->
t Base.Hashtbl.key ->
(memory_mode * Base.int) option ->
unitMirrors the retired tnode-level default_to_most_local, resolving into the placements table, with two observation guards (docs/proposals/context-scoped-memory-modes.md):
Local: Local is the unobservable class, and unlike Virtual it cannot be served by recomputation.None) at a forcing point materializes instead of defaulting to Virtual: a node that stayed undecided through optimization has no tracked computation (the virtualizer commits every stored candidate at cleanup), so a Virtual resolution would be unobservable in practice -- there would be nothing to recompute from. Effectively_constant keeps folding to Virtual: observation of constants is served by their registered host-init data.Pure counterpart of is_materialized_force; see Tnode.is_materialized_peek.