docs: plan the lowering phase (RFC-0029), and settle the spec questions it opened #22
Loading…
Reference in a new issue
No description provided.
Delete branch "rfc/0029-mir"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The front end is finished and the next phase had no design of record. This adds
the phase-opening RFC for lowering, fixes the spec's phase list to match what the
compiler will actually do, and then settles every spec question that plan opened.
0-overview.md§ Phases of CompilationPhase 4 was Optimization. On a compiler whose back end is LLVM that is the
wrong name — inlining, GVN, loop transforms and register allocation come free
with the IR, and naming the phase after them invites writing passes that
duplicate them. Phase 4 is now Lowering, and the section says plainly that
machine-level optimization is the back end's and not a phase of this compiler.
RFC-0029 — Lowering: overview
What the phase is for: drop placement on every exit path, monomorphization and
reification, the
?./??/!desugarsRFC-0016decision 3 deferred,conformance and vtable layout (
RFC-0016question 6), and devirtualization —the one optimization the phase keeps, because the types that justify it are gone
by the time LLVM sees a table-slot fetch.
It also opens five spec questions it cannot start without, each carrying a
recommendation and none decided in the RFC. The load-bearing one: the language
had no runtime-failure model at all. § Optionals said a force-unwrap "throws" in
a language with no exceptions, and whether failure unwinds decides whether MIR
has cleanup edges — the single largest structural question about the IR.
The spec settles all five, and the five they depended on
Per
RFC-0001, a gap is resolved indocs/prd/first and in its own commit.Each answer turned out to have a follow-on the spec also had to hold:
!do?try, nocatch, no landing paddebug, undefined underreleasedebugandrelease, optimization bundled in@nrecovers by-referencenew, yielding anownedfunction type#[SOA]layout or addressing?@arr[i]?sema(interface, table)pairs, identity by addressis, interface-to-interface narrowing, a fallibleasTwo of those deserve their reasoning stated rather than tabulated:
@nrecovers by-reference behaviour; nothing recovers a private copy from alanguage that captures by reference. The environment lives in the creating
frame, so a capturing lambda may be called and passed down but not returned or
stored — and one that must outlive its scope is written
new (x) -> ..., whichallocates with the word that means allocation everywhere else. That makes
owneda type modifier as well as a field one, on function types andnothing else.
yes leaves the caller holding a reference it still cannot dispatch through.
Storing the table means asking
s is IShapeand using the answer are onelookup — which is what pays for interface-to-interface narrowing and for the
fallible
as, whose rule is one sentence:x as Tis fallible exactly whenx is Tis a legal test.The grammar carries two new forms:
ownedbefore a function-type as its owntype-atomarm (deliberately not atype-modifier, which would admitowned int), andnewbefore afunc-value.RFC-0029 catches up
Four of the ten rulings went against the RFC's own recommendation, so the
phase is larger than planned:
Terminator::Trapis gone. New decision 8 fences unwinding instead: aCallgains an optional unwind successor, a failure site terminates inUnwind, a cleanup chain ends inResume, and a cleanup block may neverbranch back into the normal CFG — which
verify()now checks, becauseuncatchable is the property that keeps the IR this small.
Dropstays astatement, and can, because Catlang has no user-written destructor — teardown
is generated frees, which cannot fail. No drop flags survives too: a cleanup
path is entered from one program point, not a join, so liveness there is still
static. The cost lands in duplicated cleanup chains instead.
#[SOA]out of the no-layout fence, and records thatde-structuring costs a lowering rule and zero IR — no strided place, no second
pointer form. Decision 10 makes the build profile a lowering input, and the
fixture contract grows a fourth point pinning fixtures to
debug.IsInterfacebecomesFindTable; a fourth HIR-diff note records that lambdasneed no
Closurervalue either.What this leaves for
semaThree rulings landed upstream of the phase that asked for them. They are not
amendments —
RFC-0019,0021,0023and0026are implemented and stayimplemented.
RFC-0037–0039are reserved for the new front-end work: captures(the one with reach — an
ownedfunction type is a new shape inRFC-0018'sType, and anownedlocal is the first binding outside a struct field whoseLive/Empty state
RFC-0026has to follow), the fallibleas, and the#[SOA]addressing refusal.
None of the three blocks
RFC-0030orRFC-0031. All three blockRFC-0036,because the manifest flip asserts every fixture lowers, and a fixture lowers only
if it type-checked first.
Docs only — no code, no fixtures, no manifest rows; those land with the RFCs that
implement each ruling. Suite untouched,
.known-redstill empty. Numbers0030–0036 are reserved by the plan, as
RFC-0016reserved 0017–0028.RFC-0029 could not start without answers to five gaps in docs/prd/, and each answer opened one more. This lands all ten in the spec, where RFC-0001 says a gap is resolved, so the lowering RFCs can cite rather than decide. Runtime failure (0-overview.md, new section). "An error will be thrown" named nothing: the language has no exceptions, no panic, no abort. A failure now unwinds, running every destructor on the way up, and cannot be caught. Uncatchable is what keeps the cost to cleanup blocks and a resume — no landing pad, no try/catch, and failure stays out of the business of signalling anything. Array bounds (language-spec.md, Arrays > Bounds). Checked under debug, unchecked under release. An array already carries its length, which is the whole reason it is not a C array, so the bound is at hand. That needed a concept the spec did not have: Targets > Build Profiles, two profiles, and bounds are the only semantics they change. Lambda captures (Functions > Captures, Escaping the Enclosing Scope). By value, because @n recovers by-reference and nothing recovers by-value from the other default. The environment lives in the creating frame, so a capturing lambda may be called and passed down but not returned or stored; one that must outlive its scope is written `new (x) -> ...`, which allocates and yields an `owned` function type. `owned` therefore becomes a type modifier as well as a field one, on function types and nothing else. #[SOA] (Structs). Transparency holds except for the one operation that cannot be given honestly: the address of a whole element. Reading an element and addressing a field both stay legal; a pointer to the element does not, because the alternative is a pointer to a temporary whose writes go nowhere. The per-type descriptor (0-overview.md, What a descriptor holds). A count and a run of (interface, table) pairs, identified by address, so separate compilation and externally-registered impls need no numbering. Storing the table, not just the identity, is what makes `s is IShape` usable: the lookup that answers the question also produces the reference. That pays for interface-to-interface narrowing and for Type Conversions > Fallible Conversions, whose rule is one sentence — `x as T` is fallible exactly when `x is T` is a legal test. The grammar carries two new forms: `owned` before a function-type as its own type-atom arm rather than a type-modifier (which would admit `owned int`), and `new` before a func-value, told apart from the other new-value arms by the "(" that neither of them can start with. No fixtures or manifest rows here; those land with the RFCs that implement each ruling.docs: plan the lowering phase (RFC-0029), retire "Optimization"to docs: plan the lowering phase (RFC-0029), and settle the spec questions it opened