rfc/0029-lowering #23
Loading…
Reference in a new issue
No description provided.
Delete branch "rfc/0029-lowering"
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?
ason a union fallible even when it cannot fail 986e2ffd96ison anany5314b3f8ca§ Memory Model promised that "owned data cannot simply memory-leak or double-free by forgetting a manual teardown" and then left copying unspecified. Copying is where the promise fails, and it fails without any `delete` in the program: struct M { owned d: ?*T; } function mk() -> M { let m: M; m.d = new T; return m; } function f() -> void { let q = mk(); } `mk`'s scope exit destroys `m`, freeing the `T`; `f` receives a struct whose owned field is dangling, and `f`'s scope exit frees it again. Verified: the front end accepts all of it silently today. Destruction follows scope, so reading an owning value out of a place makes two owners of one target, and neither carries anything that says the other exists. There is no reading of that copy which leaves exactly one owner, because Catlang has no move semantics — RFC-0029 decision 7 records that `release` is the explicit way to hand a target on and everything else copies. So a value whose type transitively owns may not be read out of a place: binding it to a second name, passing it by value, returning a binding that holds one, and destructuring a tuple holding one are static errors. The rule is about the **read**, not about an owning type in value position. `return new (f: f32) -> f * by;` from § Escaping the Enclosing Scope stays legal — a fresh value handed straight on has no first owner for a second to sit beside — while `const f = new …; return f;` does not. Getting that distinction wrong would have contradicted the spec's own example. The rule is on the type rather than the value's state, so it is decidable from a declaration. The natural relaxation is a move rule, letting a `return` empty its source. It is deliberately not adopted: a move needs use-after-move tracking, which is the class of analysis § Deliberate scope rejects, and this refusal is what the existing text already requires. Found by the adversarial review of RFC-0032, which reached the shape from drop placement rather than from types. No fixture goes red — no `semantics/` fixture reads an owning value out of a place. The negative fixture and its manifest row land with RFC-0043, which implements the refusal. Spec-only, in its own commit, per RFC-0001.RFC-0024, semantic analysis. `BodyCtx::member_of` handed a struct field's declared type back verbatim: return Some(Member::Field(FieldId(index as u32), field.ty.clone())); A field's declared type is written in the *struct's* placeholders, so it only says what `base.member` is once the instantiation's arguments have been put through it. This is one arm of a match getting a treatment its sibling already gets: the `Member::Intrinsic` arm a few lines away substitutes an array intrinsic's element type for exactly this reason. The `Type::Named` binding above discarded the arguments (`let Type::Named(def, _)`), so they were not even in scope to substitute with. Before, all three accepted with zero diagnostics: struct Box<T> { v: T; } let b: Box<f32>; const x: bool = b.v; -> (global #10 (. (global #9):(#8 f32) .0):'0) struct Box<T> { v: T; } function f(b: Box<f32>) -> bool { return b.v; } -> (return (. (var %0):(#8 f32) .0):'0) struct Pair<A,B> { a: A; b: B; } let p: Pair<int,float>; const y: bool = p.a; -> (global #10 (. (global #9):(#8 s32 f32) .0):'0) After, `b.v` is `f32` and each is refused with "expected `bool`, found `f32`". Beyond the missing diagnostic, the stranded `Type::Var` is what RFC-0030's verifier (rules 6 and 10) forbids anywhere in a lowered body. RED first, at both levels: - `tests/fixtures/semantics/generic-structs.cat`, lifted from language-spec.md's own `struct Span<T> { start: T; end: ?T; }` (Derived > Arrays > Slices and Ranges) — the spec's declaration of the compiler-provided `Span`, so the fixture supplies the seed rather than shadowing it. It was rejected with "`first` has no type: nothing worked out what its initializer produces". Registered in the feature manifest, which had no row exercising a generic struct beyond its declaration. - Three inline tests in `body::tests`: positional substitution over one and two parameters, a field whose type only *mentions* a parameter (`[]T`, `*T`), a field with no parameter (unchanged), a nested instantiation, and the diagnostic that was being swallowed. The third test is the sibling audit rather than a RED case: inside a member function the receiver's arguments are its own placeholders, so `this.v` on `Box<'0>` is still `'0` and the fix must not disturb it. `direct_call` binds type arguments from the written argument types alone (`generics::infer_type_args`); the receiver contributes none. That is unobservable today because pass 1 does not scope a struct's type parameters over its namespace's functions — `function Box::get(this) -> T` is refused with "unknown type `T`" — so no generic method signature exists to substitute into. Closing it is a collect change plus a `direct_call` change, not this one-line shape, so it is left as it stands and pinned by the test. `struct_literal` holds the same verbatim `field.ty.clone()`, but is unreachable for an instantiated generic: the parser has no expression form for `Box<f32> { ... }` and the un-argumented `new Box { ... }` is already diagnosed. No existing fixture changed behaviour: `slices.cat` is the only other fixture declaring a generic struct and it never reads a field, and `spans.cat` / `object-model-functions.cat` carry implicit generics over the field-less intrinsic `Span`.`let x: T;` had no meaning. § Default Initialization defines a *type expression in value position* — `const v = Vector3;` — and says nothing about a declaration that names a type and stops. The gap was load-bearing in three places at once, and each found it independently: - RFC-0026 recorded that "there is no way to reach the live state on a stack owned field", because a struct with a non-optional owned pointer has no default and `new P { … }` yields a `*P` whose fields it cannot reach. - RFC-0032 found that drop placement emits a teardown over storage that never held a value. - `tests/fixtures/semantics/release.cat` — a green fixture lifted from § Structs — writes `let t: Transform;` and then reads `t.rotation`, putting a value on the stack that § Default Initialization says cannot go there and reading a field nothing wrote. The ruling: a declaration without an initializer declares storage and does not initialize it, and **a binding must be assigned before it is read**. Assignment is tracked per field, so a struct with no default is built one field at a time — which is what gives such a type a way to exist on the stack, and what gives the owned-rebinding rule an empty state to start from. An owned field that was never assigned owns nothing, so its first assignment needs no `release`. The neighbouring claim that such a struct "cannot be stack-allocated" is corrected: it cannot be *default*-initialized. Two bounds keep the analysis small. A binding assigned on one path and not another is not readable. And where the type owns, the paths must agree at the join rather than at a later read — because destruction follows scope, so a value whose existence depended on which path ran would need a run-time flag, which is the one thing the destruction rules exist to avoid. This is a forward pass over the control flow RFC-0026 already walks, not a new analysis, and it is definite assignment rather than a lifetime or borrow check — § Deliberate scope's fence is untouched. `release.cat` goes red under this rule and is owed an edit: assign both fields before releasing one. That edit lands with RFC-0043, which implements the analysis. No other fixture is affected. Spec-only, in its own commit, per RFC-0001.7277846630be9b349RFC-0029 reserved seven numbers for the lowering phase and three for the front-end work it pushed upstream, the way RFC-0016 reserved RFC-0017..0028. This writes all ten, amends RFC-0029 against what writing them found, and indexes them. Lowering (`mir`): 0030 the IR -- blocks, terminators, places, `dump()`, `verify()`, the crate 0031 HIR to a CFG -- the cursor, representation changes, narrowing, short-circuits, `switch` fall-through, bounds checks, `#[SOA]` 0032 drops on every exit path, and the cleanup chain behind every unwind 0033 interface conformance and table layout -- RFC-0016's question 6 0034 monomorphization, reification, and the per-type descriptors 0035 devirtualization -- the phase's one transformation 0036 wiring, the driver, the manifest flip, and what still does not lower Front-end work the phase asked for (`sema`): 0037 captures, the escape refusal, and the `owned` function type 0038 the fallible `as`, and narrowing to another interface 0039 refusing the address of an `#[SOA]` element Each was drafted, adversarially reviewed, revised against the findings, and then checked twice for cross-document consistency. The reviews are the reason the set is worth having: they found a use-after-free in RFC-0037's first draft, an ordering in RFC-0038 that made the spec's own `bool | int` union silently infallible, a predicate in RFC-0039 whose refusal leaked through generic struct fields, and -- in RFC-0030 and RFC-0031, which are mine -- fourteen defects, including a verifier rule that rejected five currently-green fixtures and a narrowing lowering that silently copied a two-word interface reference into a one-word pointer. Eleven of RFC-0029's ten locked decisions survive; decision 9 gained the scatter, the store-through-a-call and the `delete`, which it had not named. Its IR sketch changed in eighteen places -- the sharpest being that `Rvalue::InterfaceRef` could not express the spec's own interface-to-interface narrowing, where the table comes back from a runtime lookup rather than being a compile-time constant. RFC-0029's table of what `sema` still owes grew from three rows to seven, plus ten defects in code that is already implemented and two missing prelude rows. That growth is the honest result of running the front end instead of reading its documentation: `hir::Callee::Interface` is constructed nowhere, `impl` blocks are collected nowhere, and the parser implements neither `owned (f32) -> float` nor `new (f: f32) -> f * by` though the grammar documents both. Nothing here is built. RFC-0036 records what does not lower and why, and RFC-0031 carries the row-by-row account -- including the two hard stops that are language questions rather than compiler ones: a `Span<T>` whose `end: ?T` cannot hold the end of the `0..256` its own derivation rule types as `Span<u8>`, and an interpolation hole with no way to become a `string`. The spec rulings these documents rest on landed separately, in their own commits, per RFC-0001.