Skip to content

Only materialize reachable code for back passes - #3039

Draft
plajjan wants to merge 9 commits into
init-scan-orderfrom
selective-back
Draft

Only materialize reachable code for back passes#3039
plajjan wants to merge 9 commits into
init-scan-orderfrom
selective-back

Conversation

@plajjan

@plajjan plajjan commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Front passes run on 100% of a module. This changes so that for back passes, we only load and process names that are reachable.

Reachability facts are stored as exact TYDB rows and selected interfaces are reconstructed on demand. Rootless modules stop after front-pass persistence, while executable builds generate consumer-specific provider projections.

The project selection can be inspected with acton sig --reachability. Module and exact-name targets show the cached reachability facts stored in a TYDB.

@plajjan plajjan changed the title Compile deferred modules selectively Only materialize reachable code for back passes Jul 24, 2026
@plajjan
plajjan changed the base branch from main to converter-witness-forwarding August 18, 2026 15:44
Base automatically changed from converter-witness-forwarding to main August 19, 2026 11:11
Kristian Larsson added 9 commits August 19, 2026 14:48
The Vars class gives every syntax type a context-free traversal, but an
analysis that must know the environment at each reference point cannot be
expressed as freeQ: which names are local, what type a receiver has, and
whether a type constructor is merely mentioned or actually needed all
depend on the enclosing binders.

Summ is one structural fold over the typed syntax with those decisions
injected through a Walk record. Env hooks advance the environment past
statements, declaration groups, parameters, patterns and other binders;
result hooks contribute facts at variables, member selections, calls,
conditions, iterators, assignment targets and type constructors. The Dot
hook owns its receiver, so a client decides how the receiver is walked.
plainWalk is inert; a client overrides only the hooks it needs, and env
and result are type parameters so Names stays free of any environment
type. Vars is unchanged.
Deactorizer decides which actor initializer bindings become instance
state: parameters and initializer names that the actor's methods use, all
of them when the body is unfinished, and hidden names only when they are
state variables or method free variables. Move that decision next to
envOf as QuickType.actorBindings, returning both the promoted bindings and
the initializer names that stay local, so other consumers of the actor
layout share Deactorizer's rule instead of restating it.

envOf now covers Protocol and Extension declarations with an empty
environment. Both are translated away during inference, so a post-inference
caller that walks a typed suite containing them no longer hits a missing
pattern.
Split a type-checked module into rows that a later build can load
independently: one row per top-level statement, and for each class,
actor, protocol and extension a container skeleton plus one row per
method, per attribute declaration and per attribute initializer group.
The skeleton keeps the original statement order and marks each hole with
the member row that fills it, so restoring all rows reproduces the module
exactly and restoring a subset yields a partial declaration whose
remaining members are known.

Attribute initialization is split by the type checker's own rule. Its
scan of the constructor already decides which self attributes are
initialized before self can escape; scanInitPrefix now also reports how
many leading statements that prefix covers, and row partitioning uses
that count as the boundary between per-attribute initializer fragments
and the rest of the constructor. Actor bodies use the same actor state
classification as Deactorizer, so an initializer that Deactorizer would
promote to state is stored under that attribute.

The interface format version changes because rows written without this
structure cannot be loaded.
Walk each type-checked module once with its type environment and record
what every stored row depends on: which top-level names it needs, which
class members it selects, which classes it constructs, and which types it
only mentions in a signature. Names inside a class body resolve against
that class, so a bare method call from another method becomes a member
edge rather than a global one. Selection of a member on an expression is
classified from the receiver: a class-valued receiver gives a static
edge, an instance receiver a dispatch edge, and a receiver whose type is
an alias is resolved through the alias first. Conditions and iterated
expressions record the __bool__, __iter__ and __next__ methods that the
Normalizer will introduce later, since those calls do not exist in the
typed tree yet.

The walk is Names.summ with a hook record; this module keeps only the
classification. Rows are stored alongside the syntax rows: a summary per
top-level name, per member and per initializer group, the effective
provider of each method and attribute slot for a class after inheritance
and extensions, the constructor kind, the reflectable attributes, and one
aggregate summary for consumers that must materialize a whole module.
Every row is written as its own key with a stored copy of that key, so a
reader validates what it loaded; the reader and writer for all row kinds
are one keyed-row engine parameterized by a small descriptor per kind.

Missing or impossible targets are compiler errors. Selection never falls
back to keeping everything.

Hashing gains a module-owned component for statements not owned by any
name and a codegen identity derived from the compiler executable, so a
selective output is keyed by the compiler that produced it. CodeGen
emits opaque struct forward declarations for classes a projected module
only references, and leaves witness fields out of class tables so a
class's C layout does not depend on which witnesses were materialized.
Add a deterministic pretty-printer for the reachability rows of one
module or one top-level name, plus a reader that loads exactly those
rows, so the persisted dependency facts can be inspected and compared
in tests. The --reachability flag on acton sig is defined here; the
command wiring arrives with the scheduler, which also renders the closed
project selection.
Close the program roots over persisted reachability rows with a worklist
that reads only exact keys: a needed top-level name pulls its summary
and its class shape, a member edge resolves to the slot's effective
provider and pulls that member's row, a construction pulls the
constructor and the initializer groups of every attribute the program
reads, and reflection retains every reflectable attribute of every
constructed subclass. Inherited attribute declarations are kept for
layout even when their initialization is not, so a partially rendered
class keeps the field prefix of its ancestors.

Materialize the selection into partial modules for the ordinary back
pipeline: restore only the selected syntax rows from each interface,
project imports to the retained names while keeping an emptied import so
the provider's module initialization still runs, and install
declaration-only bindings for classes that are referenced but not
rendered. Missing, ambiguous or abstract targets are errors; the
projection is never widened.
Run back passes on the projected modules instead of the whole ones. Once
every front pass has finished, partition the modules into those that
must be rendered whole and those that can be projected, close the
selection over the executable roots, and enqueue one back job per
projected module with a codegen key derived from the whole selected
universe. A rootless module that was directly requested stops after its
interface unless a declared library boundary or a diagnostic output
needs its back pass.

Selective output for a provider lives in the provider's output tree but
is keyed by the consumer's universe, so concurrent builds of different
roots take a shared output lock. The language server compiles from a
compile context and a changed-path set so it can reuse the plan cache.
acton sig --reachability prints the closed project selection without
running back jobs, or the cached rows of one module or name.
Unit tests cover row round-trips, exact-key reads, worklist closure and
projection assembly. End-to-end projects compile and run through
deferred scheduling, generated C, incremental invalidation, inheritance,
native providers, output locking and consumer-specific provider output,
including aliases, optional truth conversion, inherited property layout,
module initialization, declaration-only types and rootless requests. The
important dce2 cases are carried over.
Describe the exact interface rows and their keys, the module-owned hash
component, consumer-specific projections, constructor fragments, opaque
and native boundaries, output locks and the codegen key. Missing rows
are errors and never widen the selection.
@plajjan
plajjan changed the base branch from main to init-scan-order August 19, 2026 12:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant