From 8e48748758faa978e28ae52d610ad801de17370c Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Sat, 6 Jun 2026 23:15:36 +0200 Subject: [PATCH] update drop system --- docs/REFERENCE.md | 52 +++- src/astgen.cpp | 342 +++++++++++++++++++----- src/astgen.h | 7 + src/codegen.cpp | 61 ++++- src/codegen.h | 41 +++ src/init_analysis.cpp | 54 +++- src/init_analysis.h | 15 +- src/main.cpp | 181 +++++++------ std/collections.jam | 16 +- tests/cpp/test_codegen_errors.cpp | 159 ++++++++++- tests/unit/test_conditional_methods.jam | 65 +++++ tests/unit/test_match_move.jam | 159 +++++++++++ 12 files changed, 973 insertions(+), 179 deletions(-) create mode 100644 tests/unit/test_conditional_methods.jam create mode 100644 tests/unit/test_match_move.jam diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 0569629..6152952 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -754,7 +754,7 @@ fn distance(a: Point, b: Point) f64 { return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y)); } -// Exclusive read-write, caller passes `&binding`. +// Exclusive read-write, declared at the signature only. fn scale(p: mut Point, factor: f64) { p.x = p.x * factor; p.y = p.y * factor; @@ -766,16 +766,21 @@ fn storeIn(buf: move []u8, db: mut Database) { } ``` -At the call site, a `mut` parameter is passed with `&` to make the borrow explicit: +Call sites are sigil-free for every mode — the signature's mode declaration does all +the work, and the argument is always a plain expression: ```jam var p: Point = { x: 3.0, y: 4.0 }; -scale(&p, 2.0); // mut borrow, explicit & -distance(p, otherPoint); // read-only, no sigil +scale(p, 2.0); // mut access, no sigil +distance(p, otherPoint); // read-only, no sigil ``` -`move` parameters take a plain expression, the binding is dead in the caller after the -call. +There is no reference type in jam, so `&` is not a borrow operator: it is address-of, +producing a `*mut T` / `*const T` pointer value, and is only meaningful where the +parameter's *type* is a pointer (FFI, sink out-params). Writing `&x` for a mode +parameter is a compile error. + +After a `move` call the binding is dead in the caller; reading it is a compile error. ## Exclusivity {#mvs-exclusivity} @@ -793,7 +798,7 @@ fn modify(x: mut u32, y: u32) u32 { fn caller() u32 { var n: u32 = 5; - return modify(&n, n); // error: conflicting borrows of `n` + return modify(n, n); // error: conflicting borrows of `n` } ``` @@ -822,3 +827,36 @@ fn readFile(path: []u8) i32 { A value *moved* into another function (via the `move` mode) becomes uninitialized in the caller, so the drop fires at the new owner's scope exit, never twice. + +A `move` parameter is owned by the callee: it drops when the function exits, unless +the body moves it onward — into a container slot, a struct-literal field, another +`move` call, or a `return`. Binding a bare drop-bearing value to a new name +(`var owned = c;`) or storing it (`arr[i] = c;`) is likewise a move, never a copy. +Assigning over a live drop-bearing value (`c = newCounter();`, `h.field = x;`, +`v[i] = x;`) drops the previous occupant before the store, so overwriting never +leaks. +Moving a drop-bearing value out of a `let`/`mut` parameter is rejected — those are +borrowed, not owned; declare the parameter `move` to take ownership. + +Matching a drop-bearing enum by value consumes the scrutinee: arm bindings take +ownership of the payload (they drop at the arm's end unless moved onward), arms +that don't bind drop the residual payload on entry, and a temporary scrutinee +(`match (v.pop())`) is owned by the match itself. Matching a `let`/`mut` +parameter's enum is rejected — borrowed values can't be consumed; clone the +scrutinee or take it by `move`. + +When both values are genuinely needed, clone explicitly: `x.clone()` is a deep +copy. Plain data clones for free (it is the value), structs without their own +`drop` clone field-wise, arrays clone element-wise, and a type that owns +resources (has `cfn drop`) must define `cfn clone(self: Self) Self` to say how +its resource duplicates — the compiler can clone structure, never resources. +Container clones are conditional: `Vec(T).clone()` exists exactly when `T` is +cloneable. There are no implicit clones. + +Drops stay static — jam never inserts runtime drop flags. Two restrictions on +drop-bearing bindings keep that possible: a move must be unconditional relative to the +binding's declaration (moving inside an `if` arm, loop body, or `match` arm that does +not also contain the declaration is rejected with "move it on all control-flow paths +or none"), and a moved binding cannot be re-assigned — bind a new name instead. Where +Rust would insert a runtime drop flag for a conditionally-moved value, jam rejects the +program, the same static resolution Swift's noncopyable types use. diff --git a/src/astgen.cpp b/src/astgen.cpp index dfc6739..41d840d 100644 --- a/src/astgen.cpp +++ b/src/astgen.cpp @@ -158,6 +158,22 @@ static void appendErrorHere(AstGenCtx &gctx, std::string message) { appendErrorNode(gctx, gctx.currentNode, std::move(message)); } +// Forward decl (defined near the recovery helpers below). +static JirRef recoverHere(AstGenCtx &gctx, std::string message, TypeIdx ty); + +// Method-miss reporting that understands CONDITIONAL methods: an +// instantiated generic method withdrawn for these type arguments +// replays the recorded reason instead of a bare "unknown method". +static JirRef reportMethodMiss(AstGenCtx &gctx, + const std::string &qualified) { + if (const std::string *why = gctx.ctx.getWithdrawnMethod(qualified)) { + failHere(gctx, "method `" + qualified + + "` is not available for this instantiation — " + + *why); + } + return recoverHere(gctx, "unknown method `" + qualified + "`", kNoType); +} + // Canonicalize a parser-deferred `[expr]T` (TypeKind::ArrayExpr) before // the type becomes astgen state — binding types, expected hints, field // reads. The codegen-side resolver throws when the length expression @@ -339,7 +355,9 @@ static void emitDrops(AstGenCtx &gctx, const std::vector &bindings); // `@dropInPlace(ptr)` intrinsic. Defined later; declared here so // emitDrops + astgenVarDecl can call them. static void emitDropInPlace(AstGenCtx &gctx, JirRef ptrRef, TypeIdx pointeeTy); -static bool typeNeedsDrop(AstGenCtx &gctx, TypeIdx ty); +bool typeNeedsDropInner(JamCodegenContext &ctx, TypeIdx ty); +static void emitEnumPayloadDrops(AstGenCtx &gctx, JirRef ptrRef, + const JamCodegenContext::EnumInfo *einfo); static void rejectDropBearingFieldExtract(AstGenCtx &gctx, NodeIdx exprIdx, TypeIdx resultTy, const char *verb); static void rejectAddrOfOnModeArg(AstGenCtx &gctx, NodeIdx argIdx, @@ -703,12 +721,12 @@ static void astgenReturn(AstGenCtx &gctx, const AstNode &n) { // `T.drop` or `m.T.drop`). Falls back to the codegen context's // instantiated-drops table so generic struct/enum instantiations // (Vec(i32), Holder(i32), ...) fire drops too. -static std::string lookupDropFnLLVMName(AstGenCtx &gctx, TypeIdx ty) { - const TypeKey &k = gctx.ctx.getTypePool().get(ty); +static std::string lookupDropFnLLVMName(JamCodegenContext &ctx, TypeIdx ty) { + const TypeKey &k = ctx.getTypePool().get(ty); std::string typeName; if (k.kind == TypeKind::Struct || k.kind == TypeKind::Named || k.kind == TypeKind::Enum) { - typeName = gctx.ctx.getStringPool().get(static_cast(k.a)); + typeName = ctx.getStringPool().get(static_cast(k.a)); } else { return ""; } @@ -719,30 +737,30 @@ static std::string lookupDropFnLLVMName(AstGenCtx &gctx, TypeIdx ty) { // mangling change can't silently desync. auto resolveName = [&](const std::string &name) -> std::string { const FunctionAST *fn = nullptr; - const jam::drops::DropRegistry *reg = gctx.ctx.getDropRegistry(); + const jam::drops::DropRegistry *reg = ctx.getDropRegistry(); if (reg != nullptr) { auto it = reg->find(name); if (it != reg->end()) fn = it->second; } - if (fn == nullptr) fn = gctx.ctx.lookupDropFn(name); + if (fn == nullptr) fn = ctx.lookupDropFn(name); if (fn == nullptr) return ""; - return mangledFunctionName(*fn, gctx.ctx.getTypePool(), - gctx.ctx.getStringPool()); + return mangledFunctionName(*fn, ctx.getTypePool(), + ctx.getStringPool()); }; std::string r = resolveName(typeName); if (!r.empty()) return r; - TypeIdx aliasTarget = gctx.ctx.lookupTypeAlias(typeName); + TypeIdx aliasTarget = ctx.lookupTypeAlias(typeName); if (aliasTarget != kNoType) { - const TypeKey &ak0 = gctx.ctx.getTypePool().get(aliasTarget); + const TypeKey &ak0 = ctx.getTypePool().get(aliasTarget); if (ak0.kind == TypeKind::GenericCall) { - TypeIdx resolved = gctx.ctx.resolveGenericCall(aliasTarget); + TypeIdx resolved = ctx.resolveGenericCall(aliasTarget); if (resolved != kNoType) aliasTarget = resolved; } - const TypeKey &ak = gctx.ctx.getTypePool().get(aliasTarget); + const TypeKey &ak = ctx.getTypePool().get(aliasTarget); if (ak.kind == TypeKind::Named || ak.kind == TypeKind::Struct || ak.kind == TypeKind::Enum) { std::string aliasName = - gctx.ctx.getStringPool().get(static_cast(ak.a)); + ctx.getStringPool().get(static_cast(ak.a)); r = resolveName(aliasName); if (!r.empty()) return r; } @@ -962,11 +980,11 @@ static void astgenVarDecl(AstGenCtx &gctx, const AstNode &n) { // to walk the fields at the drop site. Matches Rust's // `needs_drop()` which auto-drops a struct when any of its // fields does, even without an explicit Drop impl. - std::string dropName = lookupDropFnLLVMName(gctx, type); + std::string dropName = lookupDropFnLLVMName(gctx.ctx, type); if (!dropName.empty()) { if (gctx.dropScopes.empty()) pushDropScope(gctx); gctx.dropScopes.back().push_back({name, allocaRef, type, dropName}); - } else if (typeNeedsDrop(gctx, type)) { + } else if (typeNeedsDrop(gctx.ctx, type)) { // Struct without its own `cfn drop` but with droppable fields // (e.g. `Bus { dma: Vec(u32), ... }`). Track the binding so // scope-exit invokes `emitDropInPlace(slot, type)` which walks @@ -1163,6 +1181,11 @@ static void astgenAssign(AstGenCtx &gctx, const AstNode &n) { if (sinfo != nullptr) { const std::string qualified = sinfo->name + ".setAt"; const FunctionAST *method = gctx.ctx.getFunctionAST(qualified); + if (method == nullptr && + gctx.ctx.getWithdrawnMethod(qualified) != nullptr) { + reportMethodMiss(gctx, qualified); + return; + } if (method != nullptr && method->isCfn && method->Args.size() >= 3) { // setAt's self must be mut/move (it mutates), so we @@ -1204,7 +1227,7 @@ static void astgenAssign(AstGenCtx &gctx, const AstNode &n) { // first, old value drops, new value stores. Reassign-after-MOVE is // already rejected by init_analysis, so the destination here is // never a moved-out slot. - if (leafTy != kNoType && typeNeedsDrop(gctx, leafTy) && + if (leafTy != kNoType && typeNeedsDrop(gctx.ctx, leafTy) && assignTargetIsValueWorld(gctx, targetIdx)) { emitDropInPlace(gctx, ptrRef, leafTy); } @@ -1911,7 +1934,7 @@ static JirRef astgenArrayRepeat(AstGenCtx &gctx, const AstNode &n, // drop-bearing element type that is N owners of one payload, every // extra copy a future double-free. rustc requires `T: Copy` for // `[x; N]`; jam rejects until explicit clone() lands. - if (typeNeedsDrop(gctx, elemTy)) { + if (typeNeedsDrop(gctx.ctx, elemTy)) { failHere(gctx, "repeat literal would create " + std::to_string(count) + " owners of one drop-bearing value; initialize each " @@ -2205,6 +2228,13 @@ static JirRef astgenIndex(AstGenCtx &gctx, const AstNode &n) { JirRef atResult = emitStructCfnDispatch(gctx, sinfo, "at", recv, idxRef, {}); if (atResult != kNoJirRef) { return atResult; } + // `at` may be a WITHDRAWN conditional method for this + // instantiation (e.g. Vec(T) where T isn't cloneable): + // replay the reason instead of the generic index error. + if (gctx.ctx.getWithdrawnMethod(sinfo->name + ".at") != + nullptr) { + return reportMethodMiss(gctx, sinfo->name + ".at"); + } } } } @@ -3607,6 +3637,40 @@ static JirRef astgenMatch(AstGenCtx &gctx, const AstNode &n, TypeIdx expected) { JirRef scrut = astgenExpr(gctx, scrutIdx, kNoType); TypeIdx scrutTy = gctx.jfn.getInst(scrut).ty; + // MATCH-MOVE: matching a drop-bearing enum BY VALUE consumes the + // scrutinee (Rust semantics). The match becomes the owner: binding + // arms transfer the payload into drop-tracked bindings; arms that + // don't bind (tag-only, wildcard, no-match fallthrough) drop the + // residual payload at entry via the tag-dispatched glue. A bare + // local scrutinee's own scope-exit drop is suppressed; an rvalue + // scrutinee (`match (v.pop())`) is owned outright — which is what + // finally gives temporary enums a drop point. + const JamCodegenContext::EnumInfo *matchEinfo = + gctx.ctx.lookupEnum(scrutTy); + // Gate must MATCH the analyzer's (typeNeedsDrop alone) — a + // payload-less enum with its own cfn drop also consumes, or the two + // layers disagree (analyzer says moved, codegen drops at scope + // exit → false use-after-move rejections). + bool matchOwns = + matchEinfo != nullptr && typeNeedsDrop(gctx.ctx, scrutTy); + JirRef scrutOwned = kNoJirRef; + if (matchOwns) { + rejectDropBearingFieldExtract(gctx, scrutIdx, scrutTy, "consume"); + consumeMovedVariable(gctx, scrutIdx); + // The residual-drop glue needs a stable POINTER to the owned + // enum storage; the scrutinee here is the loaded value. One + // spill, shared by every arm's residual path. + JirInst sa{}; + sa.tag = JirTag::Alloca; + sa.ty = scrutTy; + scrutOwned = emitAllocaHoisted(gctx, sa); + JirInst sst{}; + sst.tag = JirTag::Store; + sst.a = scrutOwned; + sst.b = scrut; + emit(gctx, sst); + } + // Build merge + arm blocks up-front so we can hand refs around. JirBlockRef mergeB = gctx.jfn.pushBlock("matchend"); std::vector armBlocks; @@ -3712,9 +3776,13 @@ static JirRef astgenMatch(AstGenCtx &gctx, const AstNode &n, TypeIdx expected) { // Default block branches to merge when no wildcard arm exists, // matching the chained-CondBr semantics (non-exhaustive match // falls through; expression-form match leaves the result slot - // at its default). + // at its default). An owned scrutinee still drops its payload + // on this path. if (wildcardArmIdx < 0) { gctx.currentBlock = defaultB; + if (matchOwns) { + emitDropInPlace(gctx, scrutOwned, scrutTy); + } emitBr(gctx, mergeB); } } else { @@ -3746,6 +3814,9 @@ static JirRef astgenMatch(AstGenCtx &gctx, const AstNode &n, TypeIdx expected) { // the value after a non-exhaustive match is at fault.) if (wildcardArmIdx < 0) { gctx.currentBlock = defaultB; + if (matchOwns) { + emitDropInPlace(gctx, scrutOwned, scrutTy); + } emitBr(gctx, mergeB); } } @@ -3771,6 +3842,42 @@ static JirRef astgenMatch(AstGenCtx &gctx, const AstNode &n, TypeIdx expected) { } pushDropScope(gctx); + if (matchOwns) { + if (armBindings[i].empty()) { + // No bindings extracted the payload: this arm owns the + // residual. Full drop-in-place: the enum's own cfn drop + // (if any) plus the tag-dispatched payload glue. + emitDropInPlace(gctx, scrutOwned, scrutTy); + } else { + // Rust's E0509 analog: the payload cannot move out of + // an enum that has its OWN cfn drop — that drop expects + // the whole value intact, so extracting a field from + // under it would leave it running on a hollowed value. + if (!lookupDropFnLLVMName(gctx.ctx, scrutTy).empty()) { + failNode(gctx, arms[i].patIdx, + "cannot bind the payload out of an enum " + "that has its own `cfn drop` — the drop " + "needs the whole value; clone the payload " + "or restructure"); + } + // Bindings took the payload — they become drop-tracked + // owners in the arm's scope (same dual-flavor + // registration var-decls use). + for (const ArmBinding &bind : armBindings[i]) { + std::string dn = + lookupDropFnLLVMName(gctx.ctx, bind.type); + if (!dn.empty()) { + gctx.dropScopes.back().push_back( + {bind.name, bind.slot, bind.type, dn}); + } else if (typeNeedsDrop(gctx.ctx, bind.type)) { + gctx.dropScopes.back().push_back( + {bind.name, bind.slot, bind.type, + std::string()}); + } + } + } + } + const ArmSpec &a = arms[i]; bool armDiverged = false; for (size_t s = 0; s < a.body.size(); s++) { @@ -3969,7 +4076,7 @@ static JirRef astgenTypeMethodCall(AstGenCtx &gctx, const AstNode &n, // callee's FunctionAST via the same registry path. const FunctionAST *fn = gctx.ctx.getFunctionAST(qualified); if (fn == nullptr) { - return recoverHere(gctx, "unknown method `" + qualified + "`", kNoType); + return reportMethodMiss(gctx, qualified); } std::vector argRefs; argRefs.reserve(argCount); @@ -4001,21 +4108,21 @@ static JirRef astgenTypeMethodCall(AstGenCtx &gctx, const AstNode &n, // where a substituted field type lands as `Named("BumperI32")` and the // drop-fn lookup has to chase the alias to reach `Bumper__i32` whose // drop is registered. -static TypeIdx resolveGenericIfAny(AstGenCtx &gctx, TypeIdx ty) { - const auto &types = gctx.ctx.getTypePool(); - const auto &strings = gctx.ctx.getStringPool(); +static TypeIdx resolveGenericIfAny(JamCodegenContext &ctx, TypeIdx ty) { + const auto &types = ctx.getTypePool(); + const auto &strings = ctx.getStringPool(); constexpr int kMaxHops = 8; for (int i = 0; i < kMaxHops; i++) { const TypeKey &tk = types.get(ty); if (tk.kind == TypeKind::GenericCall) { - TypeIdx r = gctx.ctx.resolveGenericCall(ty); + TypeIdx r = ctx.resolveGenericCall(ty); if (r == kNoType || r == ty) break; ty = r; continue; } if (tk.kind == TypeKind::Named) { const std::string &name = strings.get(static_cast(tk.a)); - TypeIdx alias = gctx.ctx.lookupTypeAlias(name); + TypeIdx alias = ctx.lookupTypeAlias(name); if (alias != kNoType && alias != ty) { ty = alias; continue; @@ -4034,25 +4141,25 @@ static TypeIdx resolveGenericIfAny(AstGenCtx &gctx, TypeIdx ty) { // treated as primitives (Rust's same answer: a raw pointer doesn't // auto-drop its pointee; if the user wants that, they own a Box/Vec // whose own `cfn drop` handles deallocation). -static bool typeNeedsDrop(AstGenCtx &gctx, TypeIdx ty) { - ty = resolveGenericIfAny(gctx, ty); - if (!lookupDropFnLLVMName(gctx, ty).empty()) return true; - const TypeKey &tk = gctx.ctx.getTypePool().get(ty); +bool typeNeedsDropInner(JamCodegenContext &ctx, TypeIdx ty) { + ty = resolveGenericIfAny(ctx, ty); + if (!lookupDropFnLLVMName(ctx, ty).empty()) return true; + const TypeKey &tk = ctx.getTypePool().get(ty); // A fixed-size array owns its elements: it needs drop iff the // element type does. (Deferred `[expr]T` sizes resolve first so // field types annotated with const lengths classify correctly.) if (tk.kind == TypeKind::ArrayExpr) { - return typeNeedsDrop(gctx, gctx.ctx.resolveArrayExpr(ty)); + return typeNeedsDrop(ctx, ctx.resolveArrayExpr(ty)); } if (tk.kind == TypeKind::Array) { - return typeNeedsDrop(gctx, static_cast(tk.a)); + return typeNeedsDrop(ctx, static_cast(tk.a)); } // A payloaded enum owns whichever variant payload is live: it needs // drop iff ANY variant carries a payload type that does. - if (const auto *einfo = gctx.ctx.lookupEnum(ty)) { + if (const auto *einfo = ctx.lookupEnum(ty)) { for (const auto &v : einfo->variants) { for (TypeIdx pt : v.payloadTypes) { - if (typeNeedsDrop(gctx, pt)) return true; + if (typeNeedsDrop(ctx, pt)) return true; } } return false; @@ -4061,11 +4168,11 @@ static bool typeNeedsDrop(AstGenCtx &gctx, TypeIdx ty) { return false; } const std::string &name = - gctx.ctx.getStringPool().get(static_cast(tk.a)); - const auto *sinfo = gctx.ctx.getStruct(name); + ctx.getStringPool().get(static_cast(tk.a)); + const auto *sinfo = ctx.getStruct(name); if (sinfo == nullptr) return false; for (const auto &f : sinfo->fields) { - if (typeNeedsDrop(gctx, f.second)) return true; + if (typeNeedsDrop(ctx, f.second)) return true; } return false; } @@ -4107,7 +4214,7 @@ static bool assignTargetIsValueWorld(AstGenCtx &gctx, NodeIdx targetIdx) { if (it == gctx.localTypes.end()) return false; TypeIdx curTy = it->second; for (auto rit = steps.rbegin(); rit != steps.rend(); ++rit) { - curTy = resolveGenericIfAny(gctx, curTy); + curTy = resolveGenericIfAny(gctx.ctx, curTy); const TypeKey &k = gctx.ctx.getTypePool().get(curTy); if ((*rit)->tag == AstTag::Index) { if (k.kind != TypeKind::Array) return false; @@ -4145,7 +4252,7 @@ static bool assignTargetIsValueWorld(AstGenCtx &gctx, NodeIdx targetIdx) { // so emitDrops can re-use it to mirror Rust's "Drop::drop runs, then // fields auto-drop" sequencing. static void emitFieldDrops(AstGenCtx &gctx, JirRef ptrRef, TypeIdx pointeeTy) { - pointeeTy = resolveGenericIfAny(gctx, pointeeTy); + pointeeTy = resolveGenericIfAny(gctx.ctx, pointeeTy); const TypeKey &tk = gctx.ctx.getTypePool().get(pointeeTy); const JamCodegenContext::StructInfo *sinfo = nullptr; if (tk.kind == TypeKind::Struct || tk.kind == TypeKind::Named) { @@ -4156,7 +4263,7 @@ static void emitFieldDrops(AstGenCtx &gctx, JirRef ptrRef, TypeIdx pointeeTy) { if (sinfo == nullptr) return; for (size_t i = 0; i < sinfo->fields.size(); i++) { TypeIdx fieldTy = sinfo->fields[i].second; - if (!typeNeedsDrop(gctx, fieldTy)) continue; + if (!typeNeedsDrop(gctx.ctx, fieldTy)) continue; TypeIdx fieldPtrTy = gctx.ctx.getTypePool().intern( TypeKey{TypeKind::PtrSingle, 0, 0, fieldTy, 0}); JirInst fieldAddr{}; @@ -4202,7 +4309,7 @@ static bool emitContainerElementDrops(AstGenCtx &gctx, JirRef ptrRef, elemTy = static_cast(fk.a); } } - if (dataIdx < 0 || !typeNeedsDrop(gctx, elemTy)) return false; + if (dataIdx < 0 || !typeNeedsDrop(gctx.ctx, elemTy)) return false; // count = self.len() — receiver is the pointer-to-self we already hold. jam::abi::ParamABI recvAbi = @@ -4439,7 +4546,7 @@ static void emitEnumPayloadDrops(AstGenCtx &gctx, JirRef ptrRef, for (const auto &v : einfo->variants) { bool anyDrop = false; for (TypeIdx pt : v.payloadTypes) { - if (typeNeedsDrop(gctx, pt)) { + if (typeNeedsDrop(gctx.ctx, pt)) { anyDrop = true; break; } @@ -4473,7 +4580,7 @@ static void emitEnumPayloadDrops(AstGenCtx &gctx, JirRef ptrRef, uint64_t s = gctx.ctx.typeSize(pt); uint64_t a = gctx.ctx.typeAlign(pt); off = (off + a - 1) / a * a; - if (typeNeedsDrop(gctx, pt)) { + if (typeNeedsDrop(gctx.ctx, pt)) { JirInst offC{}; offC.tag = JirTag::Int; offC.a = static_cast(off); @@ -4507,7 +4614,7 @@ static void emitEnumPayloadDrops(AstGenCtx &gctx, JirRef ptrRef, // conformance with explicit .copy()). static void emitCloneInto(AstGenCtx &gctx, JirRef srcPtr, JirRef destPtr, TypeIdx ty) { - ty = resolveGenericIfAny(gctx, ty); + ty = resolveGenericIfAny(gctx.ctx, ty); { const TypeKey &k0 = gctx.ctx.getTypePool().get(ty); if (k0.kind == TypeKind::ArrayExpr) { @@ -4516,7 +4623,7 @@ static void emitCloneInto(AstGenCtx &gctx, JirRef srcPtr, JirRef destPtr, } // Plain data: bitwise copy is a true value copy. - if (!typeNeedsDrop(gctx, ty)) { + if (!typeNeedsDrop(gctx.ctx, ty)) { JirInst ld{}; ld.tag = JirTag::Load; ld.a = srcPtr; @@ -4561,14 +4668,106 @@ static void emitCloneInto(AstGenCtx &gctx, JirRef srcPtr, JirRef destPtr, return; } - // Enums: payload clone glue needs tag dispatch — fenced until the - // match-move milestone (the payloads only just learned to DROP). + // Enums: bitwise-copy the whole value (tag + payload bytes), then + // re-clone the live variant's droppable payload fields over the + // raw copy — tag-dispatched at the SAME byte offsets construction + // and the drop glue use. Non-droppable payload variants are + // already correct from the bitwise copy. if (const auto *einfo = gctx.ctx.lookupEnum(ty)) { - if (einfo->hasPayloadVariant) { - failHere(gctx, "enums with payloads are not yet cloneable"); + JirInst ld{}; + ld.tag = JirTag::Load; + ld.a = srcPtr; + ld.ty = ty; + JirRef whole = emit(gctx, ld); + JirInst st{}; + st.tag = JirTag::Store; + st.a = destPtr; + st.b = whole; + emit(gctx, st); + if (!einfo->hasPayloadVariant) return; + + TypeIdx u8PtrTy = gctx.ctx.getTypePool().intern( + TypeKey{TypeKind::PtrSingle, 0, 0, BuiltinType::U8, 0}); + JirInst tagFA{}; + tagFA.tag = JirTag::FieldAddr; + tagFA.a = srcPtr; + tagFA.b = 0; + tagFA.ty = u8PtrTy; + JirRef tagPtr = emit(gctx, tagFA); + JirInst tagLd{}; + tagLd.tag = JirTag::Load; + tagLd.a = tagPtr; + tagLd.ty = BuiltinType::U8; + JirRef tagVal = emit(gctx, tagLd); + + for (const auto &v : einfo->variants) { + bool anyDrop = false; + for (TypeIdx pt : v.payloadTypes) { + if (typeNeedsDrop(gctx.ctx, pt)) { + anyDrop = true; + break; + } + } + if (!anyDrop) continue; + + JirBlockRef cloneB = gctx.jfn.pushBlock("eclone"); + JirBlockRef contB = gctx.jfn.pushBlock("eclonecont"); + JirInst disc{}; + disc.tag = JirTag::Int; + disc.a = static_cast(v.discriminant); + disc.ty = BuiltinType::U8; + JirRef discRef = emit(gctx, disc); + JirInst cmp{}; + cmp.tag = JirTag::ICmpEq; + cmp.a = tagVal; + cmp.b = discRef; + cmp.ty = BuiltinType::Bool; + JirRef cmpRef = emit(gctx, cmp); + emitCondBr(gctx, cmpRef, cloneB, contB); + + gctx.currentBlock = cloneB; + JirInst sPayFA{}; + sPayFA.tag = JirTag::FieldAddr; + sPayFA.a = srcPtr; + sPayFA.b = 1; + sPayFA.ty = u8PtrTy; + JirRef sPay = emit(gctx, sPayFA); + JirInst dPayFA{}; + dPayFA.tag = JirTag::FieldAddr; + dPayFA.a = destPtr; + dPayFA.b = 1; + dPayFA.ty = u8PtrTy; + JirRef dPay = emit(gctx, dPayFA); + uint64_t off = 0; + for (TypeIdx pt : v.payloadTypes) { + uint64_t sz = gctx.ctx.typeSize(pt); + uint64_t al = gctx.ctx.typeAlign(pt); + off = (off + al - 1) / al * al; + if (typeNeedsDrop(gctx.ctx, pt)) { + JirInst offC{}; + offC.tag = JirTag::Int; + offC.a = static_cast(off); + offC.ty = BuiltinType::U64; + JirRef offRef = emit(gctx, offC); + JirInst sGep{}; + sGep.tag = JirTag::IndexAddr; + sGep.a = sPay; + sGep.b = offRef; + sGep.ty = u8PtrTy; + JirRef sF = emit(gctx, sGep); + JirInst dGep{}; + dGep.tag = JirTag::IndexAddr; + dGep.a = dPay; + dGep.b = offRef; + dGep.ty = u8PtrTy; + JirRef dF = emit(gctx, dGep); + emitCloneInto(gctx, sF, dF, pt); + } + off += sz; + } + emitBr(gctx, contB); + gctx.currentBlock = contB; } - // payload-less enums are plain data; unreachable (needsDrop - // false), but keep the shape total. return; } @@ -4602,7 +4801,7 @@ static void emitCloneInto(AstGenCtx &gctx, JirRef srcPtr, JirRef destPtr, } // Error tier: owns a resource (its own cfn drop), no clone recipe. - if (!lookupDropFnLLVMName(gctx, ty).empty()) { + if (!lookupDropFnLLVMName(gctx.ctx, ty).empty()) { failHere(gctx, "`" + sinfo->name + "` owns resources (it has `cfn drop`); define " "`cfn clone(self: Self) Self` to make it " @@ -4612,7 +4811,7 @@ static void emitCloneInto(AstGenCtx &gctx, JirRef srcPtr, JirRef destPtr, // Tier 2: field-wise structural clone. for (size_t i = 0; i < sinfo->fields.size(); i++) { TypeIdx fieldTy = - resolveGenericIfAny(gctx, sinfo->fields[i].second); + resolveGenericIfAny(gctx.ctx, sinfo->fields[i].second); TypeIdx fieldPtrTy = gctx.ctx.getTypePool().intern( TypeKey{TypeKind::PtrSingle, 0, 0, fieldTy, 0}); JirInst sfa{}; @@ -4639,7 +4838,7 @@ static void emitCloneInto(AstGenCtx &gctx, JirRef srcPtr, JirRef destPtr, // emitCloneInto. static JirRef tryLowerBuiltinClone(AstGenCtx &gctx, JirRef recvLvaluePtr, JirRef recvVal, TypeIdx recvTy) { - recvTy = resolveGenericIfAny(gctx, recvTy); + recvTy = resolveGenericIfAny(gctx.ctx, recvTy); { const TypeKey &k0 = gctx.ctx.getTypePool().get(recvTy); if (k0.kind == TypeKind::ArrayExpr) { @@ -4663,7 +4862,7 @@ static JirRef tryLowerBuiltinClone(AstGenCtx &gctx, JirRef recvLvaluePtr, } // Tier 1: plain data — clone IS the value. - if (!typeNeedsDrop(gctx, recvTy)) { + if (!typeNeedsDrop(gctx.ctx, recvTy)) { if (recvVal != kNoJirRef) return recvVal; JirInst ld{}; ld.tag = JirTag::Load; @@ -4700,7 +4899,7 @@ static void emitDropInPlace(AstGenCtx &gctx, JirRef ptrRef, TypeIdx pointeeTy) { // (Vec(u32) → Vec__u32 etc.). FieldAddr's lowering also needs the // concrete struct in `pointeeTy` so the GEP indices match the LLVM // layout. - pointeeTy = resolveGenericIfAny(gctx, pointeeTy); + pointeeTy = resolveGenericIfAny(gctx.ctx, pointeeTy); // Fixed-size arrays own their elements: drop each one that needs // it. Arrays have no cfn drop and no fields of their own, so this // is the whole story for them. @@ -4712,7 +4911,7 @@ static void emitDropInPlace(AstGenCtx &gctx, JirRef ptrRef, TypeIdx pointeeTy) { } const TypeKey &rk = gctx.ctx.getTypePool().get(arrTy); if (rk.kind == TypeKind::Array) { - if (typeNeedsDrop(gctx, static_cast(rk.a))) { + if (typeNeedsDrop(gctx.ctx, static_cast(rk.a))) { emitArrayElementDrops(gctx, ptrRef, arrTy); } return; @@ -4727,7 +4926,7 @@ static void emitDropInPlace(AstGenCtx &gctx, JirRef ptrRef, TypeIdx pointeeTy) { // call first. Then, regardless of whether the call was emitted, // walk droppable fields — matches Rust's `drop_in_place::` // which always runs Drop::drop (if any) followed by field drops. - std::string dropLLVMName = lookupDropFnLLVMName(gctx, pointeeTy); + std::string dropLLVMName = lookupDropFnLLVMName(gctx.ctx, pointeeTy); if (!dropLLVMName.empty()) { StringIdx symId = gctx.ctx.getStringPool().intern(dropLLVMName); JirInst drop{}; @@ -5155,7 +5354,7 @@ static bool isDropBearingFieldExtract(AstGenCtx &gctx, NodeIdx exprIdx, // Only locals/params — `Color.Red` and module paths also // parse as MemberAccess on a Variable root. if (gctx.locals.find(root) == gctx.locals.end()) return false; - return typeNeedsDrop(gctx, resultTy); + return typeNeedsDrop(gctx.ctx, resultTy); } return false; // Index / Deref / call in the path: pointer world } @@ -6012,8 +6211,7 @@ static JirRef astgenCall(AstGenCtx &gctx, const AstNode &n, JirRef destPtr) { std::string qualified = recvName + "." + methodName; const FunctionAST *method = gctx.ctx.getFunctionAST(qualified); if (method == nullptr) { - return recoverHere(gctx, "unknown method `" + qualified + "`", - kNoType); + return reportMethodMiss(gctx, qualified); } ParamMode mode = method->Args.empty() ? ParamMode::Let : method->Args[0].Mode; @@ -6349,6 +6547,13 @@ static JirRef astgenCall(AstGenCtx &gctx, const AstNode &n, JirRef destPtr) { std::string qualified = sinfo->name + "." + methodName; const FunctionAST *method = gctx.ctx.getFunctionAST(qualified); + // A withdrawn conditional method replays its + // reason here rather than falling through to the + // module-handle fallback's confusing error. + if (method == nullptr && + gctx.ctx.getWithdrawnMethod(qualified) != nullptr) { + return reportMethodMiss(gctx, qualified); + } if (method != nullptr && !method->Args.empty()) { // Dispatch on the full ABI — a `let self: // Self` where Self is byref arrives ByPointer, @@ -6666,6 +6871,14 @@ static JirRef astgenExpr(AstGenCtx &gctx, NodeIdx node, TypeIdx expected, lk.kind == TypeKind::PtrMany) { elemTy = static_cast(lk.a); } else { + // A struct base usually means the `v[i]` sugar's `at` + // was withdrawn for this instantiation — replay why. + if (const auto *sb = gctx.ctx.lookupStruct(baseTy)) { + if (gctx.ctx.getWithdrawnMethod(sb->name + ".at") != + nullptr) { + return reportMethodMiss(gctx, sb->name + ".at"); + } + } failHere(gctx, "astgen: lvalue index on non-array/slice/ptr-many"); } @@ -6839,6 +7052,13 @@ static JirRef astgenExpr(AstGenCtx &gctx, NodeIdx node, TypeIdx expected, } // namespace +// Global bridge for the header-declared symbol: the implementation +// lives in this file's anonymous namespace alongside the rest of the +// drop machinery. +bool typeNeedsDrop(JamCodegenContext &ctx, TypeIdx ty) { + return typeNeedsDropInner(ctx, ty); +} + JirFunction astgenMetadata(const FunctionAST &fn, JamCodegenContext &ctx) { (void)ctx; JirFunction jfn; @@ -6941,11 +7161,11 @@ void astgenBodyInto(JirFunction &jfn, const FunctionAST &fn, // before any body local, so emitDrops' reverse walk drops // locals first, params last. if (p.Mode == ParamMode::Move) { - std::string dropName = lookupDropFnLLVMName(gctx, pTy); + std::string dropName = lookupDropFnLLVMName(gctx.ctx, pTy); if (!dropName.empty()) { gctx.dropScopes.back().push_back( {p.Name, slotRef, pTy, dropName}); - } else if (typeNeedsDrop(gctx, pTy)) { + } else if (typeNeedsDrop(gctx.ctx, pTy)) { gctx.dropScopes.back().push_back( {p.Name, slotRef, pTy, std::string()}); } diff --git a/src/astgen.h b/src/astgen.h index 29bc776..87fbc36 100644 --- a/src/astgen.h +++ b/src/astgen.h @@ -51,4 +51,11 @@ JirFunction astgenMetadata(const FunctionAST &fn, JamCodegenContext &ctx); void astgenBodyInto(JirFunction &jfn, const FunctionAST &fn, JamCodegenContext &ctx); +// True when `ty` carries ownership that must drop at scope exit: its +// own `cfn drop`, drop-bearing struct fields (recursive), array +// elements, or enum variant payloads. Exposed so the mode-aware +// analysis can ask the same question codegen answers (match-move +// consumes drop-bearing enum scrutinees in BOTH layers). +bool typeNeedsDrop(JamCodegenContext &ctx, TypeIdx ty); + #endif // ASTGEN_H diff --git a/src/codegen.cpp b/src/codegen.cpp index 08e481e..7dc3373 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -1342,26 +1342,38 @@ TypeIdx JamCodegenContext::instantiateStructExpr( // resolve against that module's namespace. RAII via // try/catch: pop on every exit path. mutCtx.pushBodyModule(definingModulePath_); - // `cfn clone` instantiates CONDITIONALLY: a container's - // clone exists iff the element type is cloneable (Rust's - // `impl Clone for Vec`). A failed clone body - // withdraws its diagnostics and the method is simply not - // provided — a later `.clone()` call on this instantiation - // reports owns-resources at the call site instead. + // CONDITIONAL METHODS: every instantiated method except + // `cfn drop` exists only for the type arguments its body + // compiles AND analyzes for (Rust's `impl Clone + // for Vec` shape, generalized). A failed body withdraws + // its diagnostics, records the reason, and the method is + // simply not provided — calling it reports "not available + // for this instantiation: " at the call site. + // `cfn drop` stays unconditional: silently withdrawing a + // destructor would change ownership semantics. bool conditional = - im.clonePtr->isCfn && - im.clonePtr->Name.size() >= 6 && - im.clonePtr->Name.rfind(".clone") == - im.clonePtr->Name.size() - 6; + !(im.clonePtr->Name.size() >= 5 && + im.clonePtr->Name.rfind(".drop") == + im.clonePtr->Name.size() - 5); std::size_t diagMark = mutCtx.diagnostics().size(); + auto withdraw = [&](const std::string &reason) { + mutCtx.diagnostics().truncateTo(diagMark); + mutCtx.unregisterFunctionAST(im.clonePtr->Name); + mutCtx.recordWithdrawnMethod(im.clonePtr->Name, reason); + }; try { astgenBodyInto(im.passOneJir, *im.clonePtr, mutCtx); } catch (const AstGenAnalysisFail &) { mutCtx.popBodyModule(); clearCurrentSubst(); if (conditional) { - mutCtx.diagnostics().truncateTo(diagMark); - mutCtx.unregisterFunctionAST(im.clonePtr->Name); + std::string reason = "does not compile for these " + "type arguments"; + const auto &all = mutCtx.diagnostics().all(); + if (all.size() > diagMark) { + reason = all[diagMark].message; + } + withdraw(reason); continue; } // diagnostic already pushed; trace was attached via @@ -1369,6 +1381,31 @@ TypeIdx JamCodegenContext::instantiateStructExpr( // user sees every error in this instantiation. continue; } + // Body compiled — now run the mode-aware analysis on the + // CLONE (substituted param types/modes), so std generic + // bodies obey the same move/ownership rules user code does. + // Failures withdraw the method the same way. + if (mutCtx.analysisFns() != nullptr) { + static const std::vector kNoTokens; + auto adiags = jam::init_analysis::analyze( + *im.clonePtr, nodeStore, stringPool, kNoTokens, + mutCtx.analysisFns(), mutCtx.getDropRegistry(), + &typePool, mutCtx.analysisEnums(), + mutCtx.analysisHooks()); + if (!adiags.empty()) { + mutCtx.popBodyModule(); + clearCurrentSubst(); + if (conditional) { + withdraw(adiags[0].message); + continue; + } + for (auto &d : adiags) { + jam::SrcLoc loc{currentFile_, d.line}; + mutCtx.diagnostics().error(loc, d.message); + } + continue; + } + } mutCtx.popBodyModule(); auto diags = verifyJirFunction( im.passOneJir, &typePool, &stringPool, diff --git a/src/codegen.h b/src/codegen.h index 57d6e68..c050db0 100644 --- a/src/codegen.h +++ b/src/codegen.h @@ -13,6 +13,7 @@ #include "decl.h" #include "diagnostics.h" #include "drop_registry.h" +#include "init_analysis.h" #include "jam_llvm.h" #include #include @@ -192,6 +193,42 @@ class JamCodegenContext { return cloneRegistry_; } + // Borrowed views of the mode-aware analysis tables, so generic + // instantiation can run init_analysis on each method CLONE right + // after its body astgens (conditional methods: a clone whose body + // fails astgen OR analysis for these type args is withdrawn, and + // the reason replays at any call site). + void setAnalysisTables(const jam::init_analysis::FunctionRegistry *fns, + const jam::init_analysis::EnumVariantMap *enums) { + analysisFns_ = fns; + analysisEnums_ = enums; + } + const jam::init_analysis::FunctionRegistry *analysisFns() const { + return analysisFns_; + } + const jam::init_analysis::EnumVariantMap *analysisEnums() const { + return analysisEnums_; + } + void setAnalysisHooks(const jam::init_analysis::AnalysisHooks *h) { + analysisHooks_ = h; + } + const jam::init_analysis::AnalysisHooks *analysisHooks() const { + return analysisHooks_; + } + + // Withdrawn instantiated methods: qualified name + // ("Vec__Counter.withCapacity") -> human-readable reason. A call to + // a withdrawn method reports "not available for this instantiation" + // with the recorded reason instead of a bare "unknown method". + void recordWithdrawnMethod(const std::string &name, + std::string reason) const { + withdrawnMethods_[name] = std::move(reason); + } + const std::string *getWithdrawnMethod(const std::string &name) const { + auto it = withdrawnMethods_.find(name); + return it == withdrawnMethods_.end() ? nullptr : &it->second; + } + // Global diagnostics collector. Every pass that detects an error // (parser, astgen, init_analysis, jir_verify, generic-instantiation // codegen) pushes here instead of throwing or printing inline; the @@ -256,6 +293,10 @@ class JamCodegenContext { // drop tracking itself lives in the per-function `AstGenCtx`. const jam::drops::DropRegistry *dropRegistry = nullptr; const jam::drops::CloneRegistry *cloneRegistry_ = nullptr; + const jam::init_analysis::FunctionRegistry *analysisFns_ = nullptr; + const jam::init_analysis::EnumVariantMap *analysisEnums_ = nullptr; + const jam::init_analysis::AnalysisHooks *analysisHooks_ = nullptr; + mutable std::unordered_map withdrawnMethods_; // Global diagnostics — mutable so const accessors (`diagnostics()`) // can hand out a writable reference. Every push is a side-effect diff --git a/src/init_analysis.cpp b/src/init_analysis.cpp index cddcb90..7b636ff 100644 --- a/src/init_analysis.cpp +++ b/src/init_analysis.cpp @@ -64,9 +64,10 @@ class Analyzer { Analyzer(const NodeStore &nodes, const StringPool &strings, const std::vector &tokens, const FunctionRegistry *registry, const drops::DropRegistry *drops, const TypePool *types, - const EnumVariantMap *enums) + const EnumVariantMap *enums, const AnalysisHooks *hooks) : nodes_(nodes), strings_(strings), tokens_(tokens), - registry_(registry), drops_(drops), types_(types), enums_(enums) {} + registry_(registry), drops_(drops), types_(types), enums_(enums), + hooks_(hooks) {} std::vector run(const FunctionAST &fn); @@ -140,8 +141,20 @@ class Analyzer { const drops::DropRegistry *drops_; const TypePool *types_; const EnumVariantMap *enums_; + const AnalysisHooks *hooks_; std::vector diagnostics_; + // MATCH-MOVE oracle: does this type carry ownership (so matching it + // by value consumes the scrutinee)? Answered by codegen through the + // hook — the analyzer's own tables can't classify generic + // instantiations like Option(Counter). + bool typeOwnsDrops(TypeIdx ty) const { + if (hooks_ == nullptr || hooks_->typeNeedsDrop == nullptr) { + return false; + } + return hooks_->typeNeedsDrop(hooks_->ctx, ty); + } + // True when `enumName.variantName` names a known enum-variant // constructor (concrete enums by name; generic enum factories by // the factory fn's name). @@ -671,6 +684,25 @@ Result Analyzer::analyzeMatch(NodeIdx idx, NameMap state) { auto r = analyze(n.lhs, std::move(state)); if (r.terminated) return r; + // MATCH-MOVE: matching a drop-bearing enum by value CONSUMES the + // scrutinee — the match owns it (binding arms transfer payloads, + // non-binding arms drop the residual). The consume happens at the + // match's own depth, BEFORE the arms fork: every path through the + // match consumes, so it is unconditional. applyMoveToBinding also + // rejects consuming a borrowed (`let`/`mut` param) scrutinee. + { + const AstNode &scrutNode = nodes_.get(static_cast(n.lhs)); + if (scrutNode.tag == AstTag::Variable) { + const std::string &sname = + strings_.get(static_cast(scrutNode.lhs)); + auto vt = varTypes_.find(sname); + if (vt != varTypes_.end() && typeOwnsDrops(vt->second)) { + applyMoveToBinding(sname, static_cast(n.lhs), + r.state); + } + } + } + ExtraIdx extra = n.rhs; uint32_t armCount = nodes_.getExtra(extra + 0); @@ -885,7 +917,18 @@ void Analyzer::applyMoveToBinding(const std::string &name, NodeIdx anchor, // locals and `move`-mode params (both in declDepth_); a `let`/`mut` // param is borrowed — moving a drop-bearing value out of it would // leave the caller and the new owner both dropping the payload. - if (lookupDropFor(name) != nullptr) { + // Two oracles: the drops registry (struct-name keyed), and the + // codegen hook for everything the registry can't see — enums whose + // payloads drop, arrays of drop-bearing elements, generic + // instantiations. + bool dropBearing = lookupDropFor(name) != nullptr; + if (!dropBearing) { + auto vt = varTypes_.find(name); + if (vt != varTypes_.end()) { + dropBearing = typeOwnsDrops(vt->second); + } + } + if (dropBearing) { auto dd = declDepth_.find(name); if (dd == declDepth_.end()) { if (args_ != nullptr) { @@ -1281,8 +1324,9 @@ std::vector analyze(const FunctionAST &fn, const NodeStore &nodes, const FunctionRegistry *registry, const drops::DropRegistry *drops, const TypePool *types, - const EnumVariantMap *enums) { - Analyzer a(nodes, strings, tokens, registry, drops, types, enums); + const EnumVariantMap *enums, + const AnalysisHooks *hooks) { + Analyzer a(nodes, strings, tokens, registry, drops, types, enums, hooks); return a.run(fn); } diff --git a/src/init_analysis.h b/src/init_analysis.h index 67ea705..c02a060 100644 --- a/src/init_analysis.h +++ b/src/init_analysis.h @@ -71,6 +71,18 @@ using FunctionRegistry = std::unordered_map; using EnumVariantMap = std::unordered_map>; +// Callbacks into the codegen context for questions the analyzer can't +// answer from its own tables (generic instantiation state lives there). +// typeNeedsDrop powers MATCH-MOVE: matching a drop-bearing enum by +// value consumes the scrutinee, and the analyzer must agree with +// codegen about which types that applies to (including generic +// instantiations like Option(Counter) that the static tables can't +// classify). +struct AnalysisHooks { + void *ctx = nullptr; + bool (*typeNeedsDrop)(void *ctx, TypeIdx ty) = nullptr; +}; + // Run the definite-init analysis on a function body. Returns an empty // vector on success; on failure, the vector contains every detected // uninit-read with location info. @@ -98,7 +110,8 @@ std::vector analyze(const FunctionAST &fn, const NodeStore &nodes, const FunctionRegistry *registry = nullptr, const drops::DropRegistry *drops = nullptr, const TypePool *types = nullptr, - const EnumVariantMap *enums = nullptr); + const EnumVariantMap *enums = nullptr, + const AnalysisHooks *hooks = nullptr); } // namespace init_analysis } // namespace jam diff --git a/src/main.cpp b/src/main.cpp index 5f06e76..2e1aa37 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -672,6 +672,95 @@ static int compileAndRun(const std::string &filename, } } + // Mode-aware analysis tables, built BEFORE any body astgen runs: + // generic instantiation analyzes each method clone as it + // materializes (conditional methods), so the function registry and + // enum-variant table must already exist when the first user body + // triggers an instantiation. + jam::init_analysis::FunctionRegistry fnRegistry; + for (auto &fn : module->Functions) { fnRegistry[fn->Name] = fn.get(); } + for (auto &s : module->Structs) { + for (auto &m : s->Methods) { + fnRegistry[s->Name + "." + m->Name] = m.get(); + } + } + for (const auto &kv : resolver.getLoadedModules()) { + for (auto &fn : kv.second->Functions) { + // Non-pub fns register too: the analysis covers imported + // BODIES, and a pub fn's body calls its module's private + // helpers — their modes must resolve or moves through them + // go unseen. Flat-by-name; private-helper collisions across + // modules resolve last-wins. + fnRegistry[fn->Name] = fn.get(); + } + } + // Methods of generic struct-returning functions register under + // "GenericName.method" — modes don't depend on T. + auto registerAnonMethods = [&](const ModuleAST *m) { + const NodeStore &nsr = codegenCtx.getNodeStore(); + for (const auto &fn : m->Functions) { + if (!fn->isGeneric()) continue; + for (NodeIdx stmt : fn->Body) { + const AstNode &rn = nsr.get(stmt); + if (rn.tag != AstTag::Return) continue; + if (rn.lhs == kNoNode) break; + const AstNode &value = nsr.get(static_cast(rn.lhs)); + if (value.tag != AstTag::StructExpr) break; + uint32_t anonIdx = value.lhs; + if (anonIdx >= sharedAnonStructs.size()) break; + const StructDeclAST *anon = sharedAnonStructs[anonIdx].get(); + for (const auto &mth : anon->Methods) { + fnRegistry[fn->Name + "." + mth->Name] = mth.get(); + } + break; + } + } + }; + registerAnonMethods(module.get()); + for (const auto &kv : resolver.getLoadedModules()) { + registerAnonMethods(kv.second.get()); + } + // Enum-variant table: concrete enums by name, generic enum + // factories under the factory fn's name. + jam::init_analysis::EnumVariantMap enumVariants; + auto registerEnumVariants = [&](const ModuleAST *m) { + for (const auto &e : m->Enums) { + auto &set = enumVariants[e->Name]; + for (const auto &v : e->Variants) { set.insert(v.Name); } + } + const NodeStore &nsr = codegenCtx.getNodeStore(); + for (const auto &fn : m->Functions) { + if (!fn->isGeneric()) continue; + for (NodeIdx stmt : fn->Body) { + const AstNode &rn = nsr.get(stmt); + if (rn.tag != AstTag::Return) continue; + if (rn.lhs == kNoNode) break; + const AstNode &value = nsr.get(static_cast(rn.lhs)); + if (value.tag != AstTag::EnumExpr) break; + uint32_t anonIdx = value.lhs; + if (anonIdx >= sharedAnonEnums.size()) break; + auto &set = enumVariants[fn->Name]; + for (const auto &v : sharedAnonEnums[anonIdx]->Variants) { + set.insert(v.Name); + } + break; + } + } + }; + registerEnumVariants(module.get()); + for (const auto &kv : resolver.getLoadedModules()) { + registerEnumVariants(kv.second.get()); + } + codegenCtx.setAnalysisTables(&fnRegistry, &enumVariants); + // Match-move oracle: the analyzer asks codegen whether a scrutinee + // type owns drops (generic instantiations included). + jam::init_analysis::AnalysisHooks analysisHooks; + analysisHooks.ctx = &codegenCtx; + analysisHooks.typeNeedsDrop = +[](void *c, TypeIdx t) -> bool { + return typeNeedsDrop(*static_cast(c), t); + }; + codegenCtx.setAnalysisHooks(&analysisHooks); + // Two-pass codegen: declare every function's prototype first, then // emit bodies. Without this, calling a function defined later in the // file (or another module) would fail with "Unknown function". The @@ -979,93 +1068,9 @@ static int compileAndRun(const std::string &filename, // before any body is codegen'd. The drop registry was built // earlier (before pass 1d) so the JIR astgen could read it. { - jam::init_analysis::FunctionRegistry fnRegistry; - for (auto &fn : module->Functions) { fnRegistry[fn->Name] = fn.get(); } - for (auto &s : module->Structs) { - for (auto &m : s->Methods) { - fnRegistry[s->Name + "." + m->Name] = m.get(); - } - } - for (const auto &kv : resolver.getLoadedModules()) { - for (auto &fn : kv.second->Functions) { - // Non-pub fns register too: the analysis sweep now - // covers imported BODIES, and a pub fn's body calls its - // module's private helpers — their modes must resolve - // or moves through them go unseen. Flat-by-name, so a - // private-helper name collision across modules resolves - // last-wins (acceptable: modes rarely differ for - // same-named helpers; a per-module registry is the - // precise fix if it ever bites). - fnRegistry[fn->Name] = fn.get(); - } - } - // Methods of generic struct-returning functions (`pub fn Vec(T: - // type) type { return struct { fn push(value: move T) ... }; }`) - // register under "GenericName.method". Parameter MODES don't - // depend on T, so the un-instantiated FunctionAST is enough for - // the mode-aware callsite analysis to see `v.push(c)` as a move. - auto registerAnonMethods = [&](const ModuleAST *m) { - const NodeStore &nsr = codegenCtx.getNodeStore(); - for (const auto &fn : m->Functions) { - if (!fn->isGeneric()) continue; - for (NodeIdx stmt : fn->Body) { - const AstNode &rn = nsr.get(stmt); - if (rn.tag != AstTag::Return) continue; - if (rn.lhs == kNoNode) break; - const AstNode &value = - nsr.get(static_cast(rn.lhs)); - if (value.tag != AstTag::StructExpr) break; - uint32_t anonIdx = value.lhs; - if (anonIdx >= sharedAnonStructs.size()) break; - const StructDeclAST *anon = - sharedAnonStructs[anonIdx].get(); - for (const auto &mth : anon->Methods) { - fnRegistry[fn->Name + "." + mth->Name] = mth.get(); - } - break; - } - } - }; - registerAnonMethods(module.get()); - for (const auto &kv : resolver.getLoadedModules()) { - registerAnonMethods(kv.second.get()); - } - - // Enum-variant table: concrete enums by name, plus generic - // enum factories (`pub fn Option(T: type) type { return enum - // {...}; }`) under the factory fn's name. Lets the analyzer - // treat `Maybe.Some(c)` / `Option(T).Some(c)` payload args as - // the moves they are. - jam::init_analysis::EnumVariantMap enumVariants; - auto registerEnumVariants = [&](const ModuleAST *m) { - for (const auto &e : m->Enums) { - auto &set = enumVariants[e->Name]; - for (const auto &v : e->Variants) { set.insert(v.Name); } - } - const NodeStore &nsr = codegenCtx.getNodeStore(); - for (const auto &fn : m->Functions) { - if (!fn->isGeneric()) continue; - for (NodeIdx stmt : fn->Body) { - const AstNode &rn = nsr.get(stmt); - if (rn.tag != AstTag::Return) continue; - if (rn.lhs == kNoNode) break; - const AstNode &value = - nsr.get(static_cast(rn.lhs)); - if (value.tag != AstTag::EnumExpr) break; - uint32_t anonIdx = value.lhs; - if (anonIdx >= sharedAnonEnums.size()) break; - auto &set = enumVariants[fn->Name]; - for (const auto &v : sharedAnonEnums[anonIdx]->Variants) { - set.insert(v.Name); - } - break; - } - } - }; - registerEnumVariants(module.get()); - for (const auto &kv : resolver.getLoadedModules()) { - registerEnumVariants(kv.second.get()); - } + // fnRegistry / enumVariants were built before the body passes + // (see setAnalysisTables above) — instantiation-time analysis + // and this whole-module sweep share the same tables. auto runAnalysisIn = [&](FunctionAST *function, const std::string &file) { @@ -1073,7 +1078,7 @@ static int compileAndRun(const std::string &filename, auto diags = jam::init_analysis::analyze( *function, codegenCtx.getNodeStore(), codegenCtx.getStringPool(), tokens, &fnRegistry, &dropRegistry, - &codegenCtx.getTypePool(), &enumVariants); + &codegenCtx.getTypePool(), &enumVariants, &analysisHooks); // Funnel each init-analysis diagnostic into the unified // `jam::Diagnostics` channel so they share the same // formatting / ordering as astgen errors. diff --git a/std/collections.jam b/std/collections.jam index 8d471d7..3453d48 100644 --- a/std/collections.jam +++ b/std/collections.jam @@ -87,7 +87,9 @@ pub fn Vec(T: type) type { if (i >= self.length) { return Option(T).None(); } - return Option(T).Some(self.ptr[i]); + // Clone out: the container keeps its element, the Option + // owns an independent copy (free for plain T). + return Option(T).Some(self.ptr[i].clone()); } // `cfn at` / `cfn setAt` are the value-shaped hooks the @@ -100,9 +102,15 @@ pub fn Vec(T: type) type { // // Bounds-checked reads remain on `get(i) -> Option(T)`. cfn at(self: Self, i: u32) T { - return self.ptr[i]; - } - cfn setAt(self: mut Self, i: u32, value: T) { + // Reads CLONE the element out: the container keeps its + // copy, the caller owns an independent one. For plain T + // clone is the value itself, so v[i] on Vec(u32) is the + // same raw read as before. For non-cloneable T this method + // withdraws (conditional methods) and v[i] reads report + // why at the call site. + return self.ptr[i].clone(); + } + cfn setAt(self: mut Self, i: u32, value: move T) { // Overwriting a LIVE element drops the previous occupant // first (slots beyond length are uninitialized capacity — // dropping those would read garbage). @dropInPlace is a diff --git a/tests/cpp/test_codegen_errors.cpp b/tests/cpp/test_codegen_errors.cpp index aee9e33..1da5d8a 100644 --- a/tests/cpp/test_codegen_errors.cpp +++ b/tests/cpp/test_codegen_errors.cpp @@ -303,6 +303,13 @@ class CodegenErrorTests { testConditionalContainerClone); framework.addTest("Clone - payloaded enum clone fenced", testEnumCloneFenced); + framework.addTest( + "Conditional - withdrawn method replays reason at call site", + testWithdrawnMethodReplays); + framework.addTest("MatchMove - rejections (use-after/borrowed/cond)", + testMatchMoveRejections); + framework.addTest("MatchMove - own-cfn-drop enum edge (E0509 analog)", + testMatchMoveOwnDropEnum); } private: @@ -1279,6 +1286,10 @@ fn main() {} ASSERT_TRUE(stderrContains(r, "owns resources")); } + // Enum payload clone recurses into the payload's clone tier: a + // non-cloneable payload (own cfn drop, no cfn clone) reports + // owns-resources at the clone site. (Cloneable payloads deep-clone + // — pinned in tests/unit/test_match_move.jam.) static void testEnumCloneFenced() { auto r = compileSource("clone_enum_fenced", movePrelude() + R"( const Maybe = enum { @@ -1292,8 +1303,154 @@ fn bad(sink: *mut u32) { fn main() {} )"); ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains(r, "owns resources")); + ASSERT_TRUE(stderrContains(r, "define `cfn clone")); + } + + // Conditional generic methods: a method whose body fails astgen or + // the mode-aware analysis for these type args is WITHDRAWN; calling + // it replays the recorded reason. Three flavors: the `v[i]` sugar's + // `at` (astgen withdrawal: element not cloneable), `get` (same), + // and `filled` (ANALYSIS withdrawal: the fill duplicates a borrowed + // drop-bearing value). + static void testWithdrawnMethodReplays() { + std::string prelude = std::string(R"( +const { Vec } = import("std/collections"); +)") + movePrelude(); + auto idx = compileSource("cond_at_replay", prelude + R"( +fn bad(sink: *mut u32) u32 { + var v: Vec(Counter) = Vec(Counter).empty(); + v.push(Counter { value: 1, sink: sink }); + return v[0].value; +} +fn main() {} +)"); + ASSERT_TRUE(idx.exitCode != 0); + ASSERT_TRUE(stderrContains( + idx, "`Vec__Counter.at` is not available for this instantiation")); + ASSERT_TRUE(stderrContains(idx, "owns resources")); + + auto filled = compileSource("cond_filled_replay", prelude + R"( +fn bad(sink: *mut u32) { + var c: Counter = Counter { value: 1, sink: sink }; + var v: Vec(Counter) = Vec(Counter).filled(c, 3); +} +fn main() {} +)"); + ASSERT_TRUE(filled.exitCode != 0); + ASSERT_TRUE(stderrContains( + filled, + "`Vec__Counter.filled` is not available for this instantiation")); + ASSERT_TRUE(stderrContains(filled, "borrowed, not owned")); + + auto get = compileSource("cond_get_replay", prelude + R"( +fn bad(sink: *mut u32) u32 { + var v: Vec(Counter) = Vec(Counter).empty(); + v.push(Counter { value: 1, sink: sink }); + var got = v.get(0); + return 0; +} +fn main() {} +)"); + ASSERT_TRUE(get.exitCode != 0); + ASSERT_TRUE(stderrContains( + get, + "`Vec__Counter.get` is not available for this instantiation")); + } + + // MATCH-MOVE rejections: matching a drop-bearing enum consumes the + // scrutinee, so re-matching is use-after-move, a borrowed (let + // param) scrutinee can't be consumed, and a conditional match of an + // outer binding violates the depth rule. + static void testMatchMoveRejections() { + std::string en = movePrelude() + R"( +const Maybe = enum { + None, + Some(Counter), +}; +)"; + auto twice = compileSource("matchmove_twice", en + R"( +fn bad(sink: *mut u32) u32 { + var m: Maybe = Maybe.Some(Counter { value: 1, sink: sink }); + match (m) { _ { } } + match (m) { _ { return 1; } } + return 0; +} +fn main() {} +)"); + ASSERT_TRUE(twice.exitCode != 0); + ASSERT_TRUE(stderrContains(twice, "use of moved binding `m`")); + + auto borrowed = compileSource("matchmove_borrowed", en + R"( +fn bad(m: Maybe) u32 { + match (m) { _ { return 1; } } + return 0; +} +fn main() {} +)"); + ASSERT_TRUE(borrowed.exitCode != 0); + ASSERT_TRUE(stderrContains(borrowed, "borrowed, not owned")); + + auto cond = compileSource("matchmove_cond", en + R"( +fn bad(sink: *mut u32, doIt: bool) { + var m: Maybe = Maybe.Some(Counter { value: 1, sink: sink }); + if (doIt) { + match (m) { _ { } } + } +} +fn main() {} +)"); + ASSERT_TRUE(cond.exitCode != 0); + ASSERT_TRUE(stderrContains(cond, "move it on all control-flow paths")); + } + + // Enums with their OWN cfn drop: match-consume applies even when + // payload-less (the analyzer/codegen gates must agree — found by + // the rustc-comparison fleet), and binding the payload out from + // under the enum's own drop is rejected (Rust's E0509). + static void testMatchMoveOwnDropEnum() { + auto twice = compileSource("matchmove_owndrop_twice", R"( +extern fn puts(s: *const[] u8) i32; +const Token = enum { + Red, + Blue, +}; +cfn drop(self: mut Token) { + puts("D"); +} +fn bad() u32 { + var t: Token = Token.Red(); + match (t) { _ { } } + match (t) { _ { return 1; } } + return 0; +} +fn main() {} +)"); + ASSERT_TRUE(twice.exitCode != 0); + ASSERT_TRUE(stderrContains(twice, "use of moved binding `t`")); + + auto bind = compileSource("matchmove_owndrop_bind", R"( +extern fn puts(s: *const[] u8) i32; +const Wrapped = enum { + None, + Some(u32), +}; +cfn drop(self: mut Wrapped) { + puts("D"); +} +fn bad() u32 { + var w: Wrapped = Wrapped.Some(5); + match (w) { + Wrapped.Some(x) { return x; } + _ { return 0; } + } +} +fn main() {} +)"); + ASSERT_TRUE(bind.exitCode != 0); ASSERT_TRUE(stderrContains( - r, "enums with payloads are not yet cloneable")); + bind, "cannot bind the payload out of an enum that has its " + "own `cfn drop`")); } // Call sites are sigil-free for parameter modes; `&` is address-of diff --git a/tests/unit/test_conditional_methods.jam b/tests/unit/test_conditional_methods.jam new file mode 100644 index 0000000..18a6730 --- /dev/null +++ b/tests/unit/test_conditional_methods.jam @@ -0,0 +1,65 @@ +// Conditional generic methods: an instantiated method exists only for +// the type arguments its body compiles AND analyzes for (except cfn +// drop, which is unconditional). Vec(Counter): at/get/clone withdraw +// (Counter is not cloneable), filled withdraws (its fill duplicates a +// borrowed value) — while push/pop/setAt/drop stay available, so the +// container itself keeps working. Calls to withdrawn methods replay +// the reason (pinned in tests/cpp/test_codegen_errors.cpp). + +const { assert } = import("test"); +const { Vec } = import("std/collections"); + +const Counter = struct { value: u32, sink: *mut u32, }; +cfn drop(self: mut Counter) { + var p: *mut u32 = self.sink; + p.* = p.* + 1; +} + +const Tracked = struct { value: u32, dropSink: *mut u32, cloneSink: *mut u32, }; +cfn drop(self: mut Tracked) { + var p: *mut u32 = self.dropSink; + p.* = p.* + 1; +} +cfn clone(self: Tracked) Tracked { + var p: *mut u32 = self.cloneSink; + p.* = p.* + 1; + return Tracked { value: self.value, dropSink: self.dropSink, cloneSink: self.cloneSink }; +} + +fn basicFlow(sink: *mut u32) { + var v: Vec(Counter) = Vec(Counter).empty(); + v.push(Counter { value: 1, sink: sink }); + v[0] = Counter { value: 2, sink: sink }; + var popped = v.pop(); +} + +tfn vecCounterCoreStillWorks() { + var hits: u32 = 0; + basicFlow(&hits); + assert(hits, 2); +} + +fn getClones(drops: *mut u32, clones: *mut u32) u32 { + var v: Vec(Tracked) = Vec(Tracked).empty(); + v.push(Tracked { value: 9, dropSink: drops, cloneSink: clones }); + var got = v.get(0); + match (got) { + Option(Tracked).Some(t) { return t.value; } + _ { return 0; } + } +} + +tfn getClonesForCloneableElems() { + var drops: u32 = 0; + var clones: u32 = 0; + var got: u32 = getClones(&drops, &clones); + assert(got, 9); + assert(clones, 1); + assert(drops, 2); +} + +tfn indexReadOnPlainVecUnchanged() { + var v: Vec(u32) = Vec(u32).empty(); + v.push(42); + assert(v[0], 42); +} diff --git a/tests/unit/test_match_move.jam b/tests/unit/test_match_move.jam new file mode 100644 index 0000000..8d7f9ea --- /dev/null +++ b/tests/unit/test_match_move.jam @@ -0,0 +1,159 @@ +// MATCH-MOVE: matching a drop-bearing enum BY VALUE consumes the +// scrutinee. Binding arms own the payload (drop-tracked, droppable at +// arm exit or movable onward); non-binding arms (tag-only, wildcard) +// drop the residual payload at entry via the tag-dispatched glue; +// rvalue scrutinees (match (v.pop())) are owned by the match itself +// — which closes the last temporary-enum leak. Plus: enum payload +// deep-clone (clone counts pinned below). Rejections (use-after- +// match, borrowed scrutinee, conditional match) are pinned in +// tests/cpp/test_codegen_errors.cpp. + +const { assert } = import("test"); +const { Vec } = import("std/collections"); + +const Counter = struct { value: u32, sink: *mut u32, }; +cfn drop(self: mut Counter) { + var p: *mut u32 = self.sink; + p.* = p.* + 1; +} + +const Maybe = enum { + None, + Some(Counter), +}; + +fn consume(c: move Counter) { +} + +fn rvalueMatch(sink: *mut u32) u32 { + var v: Vec(Counter) = Vec(Counter).empty(); + v.push(Counter { value: 7, sink: sink }); + match (v.pop()) { + Option(Counter).Some(c) { return c.value; } + _ { return 0; } + } +} + +tfn rvalueMatchOwnsTheTemp() { + var hits: u32 = 0; + var got: u32 = rvalueMatch(&hits); + assert(got, 7); + assert(hits, 1); +} + +fn wildcardResidual(sink: *mut u32) { + var m: Maybe = Maybe.Some(Counter { value: 1, sink: sink }); + match (m) { + _ { } + } +} + +tfn wildcardArmDropsResidualPayload() { + var hits: u32 = 0; + wildcardResidual(&hits); + assert(hits, 1); +} + +fn tagOnlyResidual(sink: *mut u32) u32 { + var m: Maybe = Maybe.Some(Counter { value: 1, sink: sink }); + match (m) { + Maybe.None { return 9; } + _ { return 1; } + } +} + +tfn tagArmsDropResidualOnEveryPath() { + var hits: u32 = 0; + var r: u32 = tagOnlyResidual(&hits); + assert(r, 1); + assert(hits, 1); +} + +fn bindingMovedOnward(sink: *mut u32) { + var m: Maybe = Maybe.Some(Counter { value: 2, sink: sink }); + match (m) { + Maybe.Some(c) { consume(c); } + _ { } + } +} + +tfn armBindingMovesOnward() { + var hits: u32 = 0; + bindingMovedOnward(&hits); + assert(hits, 1); +} + +fn bindingDropsAtArmExit(sink: *mut u32) { + var m: Maybe = Maybe.Some(Counter { value: 3, sink: sink }); + match (m) { + Maybe.Some(c) { var x: u32 = c.value; } + _ { } + } +} + +tfn armBindingDropsAtArmExit() { + var hits: u32 = 0; + bindingDropsAtArmExit(&hits); + assert(hits, 1); +} + +fn noneThroughBindingMatch(sink: *mut u32) { + var m: Maybe = Maybe.None(); + match (m) { + Maybe.Some(c) { var x: u32 = c.value; } + _ { } + } + var c2: Counter = Counter { value: 1, sink: sink }; +} + +tfn noneVariantNothingExtra() { + var hits: u32 = 0; + noneThroughBindingMatch(&hits); + assert(hits, 1); +} +const Tracked = struct { value: u32, dropSink: *mut u32, cloneSink: *mut u32, }; +cfn drop(self: mut Tracked) { + var p: *mut u32 = self.dropSink; + p.* = p.* + 1; +} +cfn clone(self: Tracked) Tracked { + var p: *mut u32 = self.cloneSink; + p.* = p.* + 1; + return Tracked { value: self.value, dropSink: self.dropSink, cloneSink: self.cloneSink }; +} + +const MaybeT = enum { + None, + Some(Tracked), +}; + +fn cloneEnum(drops: *mut u32, clones: *mut u32) u32 { + var m: MaybeT = MaybeT.Some(Tracked { value: 6, dropSink: drops, cloneSink: clones }); + var n: MaybeT = m.clone(); + match (n) { + MaybeT.Some(t) { return t.value; } + _ { return 0; } + } +} + +tfn enumCloneDeep() { + var drops: u32 = 0; + var clones: u32 = 0; + var got: u32 = cloneEnum(&drops, &clones); + assert(got, 6); + assert(clones, 1); + assert(drops, 2); +} + +fn cloneNone(drops: *mut u32, clones: *mut u32) { + var m: MaybeT = MaybeT.None(); + var n: MaybeT = m.clone(); +} + +tfn enumCloneNoneFree() { + var drops: u32 = 0; + var clones: u32 = 0; + cloneNone(&drops, &clones); + assert(clones, 0); + assert(drops, 0); +} -- 2.51.2