diff --git a/Makefile b/Makefile index 5290dbc..0242894 100644 --- a/Makefile +++ b/Makefile @@ -189,7 +189,7 @@ test-comptime: build @echo "" @echo "Building and running Comptime C++ tests..." @clang++ -c ./tests/cpp/test_comptime.cpp -o $(OUT)/test_comptime.o `$(LLVM_CONFIG) --cxxflags` -fexceptions $(OPTFLAGS) - @clang++ -o $(OUT)/comptime_tests $(OUT)/test_comptime.o $(OUT)/comptime.o $(OUT)/diagnostics.o + @clang++ -o $(OUT)/comptime_tests $(OUT)/test_comptime.o $(OUT)/comptime.o $(OUT)/diagnostics.o $(OUT)/jam_llvm.o `$(LLVM_CONFIG) --ldflags --libs --libfiles --system-libs` @$(OUT)/comptime_tests test-print: build diff --git a/src/codegen.cpp b/src/codegen.cpp index 6814477..02160aa 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -472,12 +472,11 @@ JamCodegenContext::getFunctionAST(const std::string &name) const { // std/collections.jam (where Vec lives) regardless of whether the // caller module independently imports it. // - // Falls back to the flat global map for two cases the per-module - // table doesn't cover: (a) generic instantiations registered by - // mangled name (`Vec__u32.withCapacity` etc.) that live in the - // global map but not in any source module's pub-fn table; (b) - // names looked up outside any instantiation body (normal entry- - // module function bodies). + // The final global map below holds only NON-import entries: plain fns, + // struct methods, and generic instantiations registered by mangled name + // (`Vec__u32.withCapacity`). Import-handle flats (`handle.X`) live in the + // per-module `moduleImports_` tables instead, so an imported body can't + // reach the entry module's imports through this fallback. if (!bodyModuleStack_.empty()) { const std::string &defMod = bodyModuleStack_.back(); if (!defMod.empty()) { @@ -488,6 +487,16 @@ JamCodegenContext::getFunctionAST(const std::string &name) const { } } } + // Qualified handle calls (`std.fmt.print`, `fmt.print`) resolve against + // the CURRENT body's module imports -- the entry module / entry-defined + // generics use the "" key. A body never sees another file's imports. + { + auto mIt = moduleImports_.find(currentBodyModule()); + if (mIt != moduleImports_.end()) { + auto qIt = mIt->second.qualFns.find(name); + if (qIt != mIt->second.qualFns.end()) return qIt->second; + } + } auto it = functionAsts.find(name); return (it == functionAsts.end()) ? nullptr : it->second; } @@ -505,20 +514,20 @@ const std::string &JamCodegenContext::currentBodyModule() const { return bodyModuleStack_.empty() ? kEmpty : bodyModuleStack_.back(); } -void JamCodegenContext::registerImportHandle(const std::string &handle, - const std::string &modulePath) { - importHandles_[handle].modulePath = modulePath; -} - -void JamCodegenContext::registerPrivateName(const std::string &handle, - const std::string &name) { - importHandles_[handle].privateNames.insert(name); -} - const JamCodegenContext::ImportHandleInfo * JamCodegenContext::getImportHandle(const std::string &handle) const { - auto it = importHandles_.find(handle); - return (it == importHandles_.end()) ? nullptr : &it->second; + // Resolve against the CURRENT body's module imports only -- the entry + // module (and entry-defined generics) use the "" key; an imported body + // (e.g. Bus.drop in bus.jam, lowered under pushBodyModule("bus")) uses its + // own key. There is NO cross-module fallback: a handle absent from the + // declaring module's imports is an error, never resolved against the + // entry/root module's imports. + auto mIt = moduleImports_.find(currentBodyModule()); + if (mIt != moduleImports_.end()) { + auto hIt = mIt->second.handles.find(handle); + if (hIt != mIt->second.handles.end()) return &hIt->second; + } + return nullptr; } void JamCodegenContext::registerModuleNamespace(ModuleNamespace ns) { diff --git a/src/codegen.h b/src/codegen.h index 1e4f908..a76ed7c 100644 --- a/src/codegen.h +++ b/src/codegen.h @@ -288,24 +288,59 @@ class JamCodegenContext { std::string modulePath; std::unordered_set privateNames; }; - void registerImportHandle(const std::string &handle, - const std::string &modulePath); - void registerPrivateName(const std::string &handle, - const std::string &name); + // Resolves `handle` against the CURRENT body module's imports (the "" + // scope for the entry module). No cross-module fallback -- see the .cpp. const ImportHandleInfo *getImportHandle(const std::string &handle) const; std::string formatNamespaceLookupError(const std::string &kind, const std::string &qualified) const; + // --- Per-declaring-module import bindings --- + // A function/method body compiled under pushBodyModule(M) -- e.g. an + // imported pub/cfn body like `Bus.drop` in bus.jam -- resolves its + // qualified imports (`std.fmt.print`) against M's OWN imports, never the + // importer's. Every module gets its own scope here, keyed by module path; + // the entry module uses the "" key (the key currentBodyModule() returns for + // an empty stack, and for entry-defined generics whose modulePath is ""), + // so a body never sees another file's imports. There is no cross-module + // fallback: a qualified name absent from the declaring module's imports is + // an error, not a lookup in the entry/root module. + // + // This is the qualified-import analogue of the BARE-name routing in + // getFunctionAST (see moduleNamespaces_). + struct ModuleImports { + std::unordered_map handles; + std::unordered_map qualFns; + std::unordered_map typeAliases; + }; + void registerScopedHandle(const std::string &owner, + const std::string &handle, + const std::string &modulePath) { + moduleImports_[owner].handles[handle].modulePath = modulePath; + } + void registerScopedPrivateName(const std::string &owner, + const std::string &handle, + const std::string &name) { + moduleImports_[owner].handles[handle].privateNames.insert(name); + } + void registerScopedHandleFn(const std::string &owner, + const std::string &key, const FunctionAST *fn) { + moduleImports_[owner].qualFns[key] = fn; + } + void registerScopedTypeAlias(const std::string &owner, + const std::string &key, TypeIdx target) { + moduleImports_[owner].typeAliases[key] = target; + } + // Per-loaded-module namespace. Indexes the module's `pub` members // by source-level name so that member access on a Module value // (e.g. `std.fmt`) can resolve to a concrete FunctionAST, TypeIdx, // or another Module value (re-exports). // // Populated when a module is resolved (Phase 2+). Distinct from - // `importHandles_`, which keys by the *binding-site* handle name - // (e.g. `fmt` from `const fmt = import("fmt");`); ModuleNamespace - // keys by the *resolved canonical path* (e.g. "fmt", "std/fmt") so - // re-exports and aliases all converge on one entry per file. + // `moduleImports_`, whose per-module `handles` key by the *binding-site* + // handle name (e.g. `fmt` from `const fmt = import("fmt");`); + // ModuleNamespace keys by the *resolved canonical path* (e.g. "fmt", + // "std/fmt") so re-exports and aliases all converge on one entry per file. struct ModuleNamespace { // Canonical resolved path (e.g. "fmt", "std/fmt"). Same string // stored in TypeKind::Module's `a` field. @@ -341,7 +376,12 @@ class JamCodegenContext { private: std::unordered_map functionAsts; - std::unordered_map importHandles_; + // Per-declaring-module import bindings, keyed by module path -- "" is the + // entry module (the key currentBodyModule() returns for an empty stack and + // for entry-defined generics). Consulted by getImportHandle / + // getFunctionAST / lookupTypeAlias with NO cross-module fallback for + // import-derived names. See ModuleImports. + std::unordered_map moduleImports_; // Resolved canonical path -> namespace decl table. See ModuleNamespace. std::unordered_map moduleNamespaces_; // Stack of `modulePath` strings for the bodies currently being lowered. @@ -449,6 +489,16 @@ class JamCodegenContext { typeAliases_[name] = target; } TypeIdx lookupTypeAlias(const std::string &name) const { + // Import-handle aliases (`handle.Type`) resolve against the CURRENT + // body's module imports -- the entry module / entry-defined generics + // use the "" key. No cross-module fallback for these. + auto mIt = moduleImports_.find(currentBodyModule()); + if (mIt != moduleImports_.end()) { + auto tIt = mIt->second.typeAliases.find(name); + if (tIt != mIt->second.typeAliases.end()) return tIt->second; + } + // Local `const Name = T` aliases live in the flat table (not import- + // derived), and stay reachable from any body. auto it = typeAliases_.find(name); if (it != typeAliases_.end()) return it->second; return kNoType; diff --git a/src/drop_registry.cpp b/src/drop_registry.cpp index d5b4f4c..60061e0 100644 --- a/src/drop_registry.cpp +++ b/src/drop_registry.cpp @@ -45,9 +45,8 @@ static void considerDropCandidate(const FunctionAST *fn, const TypePool &types, registry[structName] = fn; } -DropRegistry buildDropRegistry(const ModuleAST &module, const TypePool &types, - const StringPool &strings) { - DropRegistry registry; +void addDropCandidates(DropRegistry ®istry, const ModuleAST &module, + const TypePool &types, const StringPool &strings) { // Top-level `fn drop(self: mut T)` declarations. for (const auto &fn : module.Functions) { considerDropCandidate(fn.get(), types, strings, registry); @@ -62,6 +61,12 @@ DropRegistry buildDropRegistry(const ModuleAST &module, const TypePool &types, considerDropCandidate(m.get(), types, strings, registry); } } +} + +DropRegistry buildDropRegistry(const ModuleAST &module, const TypePool &types, + const StringPool &strings) { + DropRegistry registry; + addDropCandidates(registry, module, types, strings); return registry; } diff --git a/src/drop_registry.h b/src/drop_registry.h index 15aad7e..cef7f44 100644 --- a/src/drop_registry.h +++ b/src/drop_registry.h @@ -48,6 +48,13 @@ using DropRegistry = std::unordered_map; DropRegistry buildDropRegistry(const ModuleAST &module, const TypePool &types, const StringPool &strings); +// Add `module`'s drop fns to an existing registry. Used to fold imported +// modules' drops in, so a drop site in one module fires the destructor of a +// type defined (and imported) from another (e.g. `Bus.drop` in bus.jam, +// dropped in main.jam). +void addDropCandidates(DropRegistry ®istry, const ModuleAST &module, + const TypePool &types, const StringPool &strings); + } // namespace drops } // namespace jam diff --git a/src/main.cpp b/src/main.cpp index da01c2e..90567b3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -411,43 +411,54 @@ static int compileAndRun(const std::string &filename, // Register every flat `handle.X` mapping for a given (handle name, // resolved module). Shared by direct-import bindings and module- // valued destructuring bindings (`const {fmt} = import("std");`). + // `owner` is the module whose imports these are -- and the scope key its + // bodies resolve against. "" is the entry module: its bodies compile with + // an empty bodyModuleStack_ (which maps to the "" key), and entry-defined + // generics carry an empty modulePath (same key). EVERY module -- entry + // included -- gets its OWN scope in moduleImports_, so a body resolves its + // qualified imports against its own module and never inherits another's. + // There is deliberately NO cross-module fallback (see getImportHandle): a + // qualified name absent from the declaring module's imports is an error, + // not a lookup in the entry/root module. auto registerHandleFlats = [&](const std::string &handle, const std::string &modulePath, - ModuleAST *importedModule) { - codegenCtx.registerImportHandle(handle, modulePath); + ModuleAST *importedModule, + const std::string &owner) { + codegenCtx.registerScopedHandle(owner, handle, modulePath); + auto regFn = [&](const std::string &key, const FunctionAST *fn) { + codegenCtx.registerScopedHandleFn(owner, key, fn); + }; + auto regPriv = [&](const std::string &name) { + codegenCtx.registerScopedPrivateName(owner, handle, name); + }; auto aliasNamed = [&](const std::string &bare) { TypeIdx target = codegenCtx.getTypePool().internNamed( codegenCtx.getStringPool().intern(bare)); - codegenCtx.registerTypeAlias(handle + "." + bare, target); + codegenCtx.registerScopedTypeAlias(owner, handle + "." + bare, + target); }; for (auto &func : importedModule->Functions) { - if (func->isPub) { - codegenCtx.registerFunctionAST(handle + "." + func->Name, - func.get()); - } else { - codegenCtx.registerPrivateName(handle, func->Name); - } + if (func->isPub) regFn(handle + "." + func->Name, func.get()); + else regPriv(func->Name); } for (auto &s : importedModule->Structs) { if (s->isPub) { aliasNamed(s->Name); for (auto &m : s->Methods) { - if (m->isPub) { - codegenCtx.registerFunctionAST( - handle + "." + s->Name + "." + m->Name, m.get()); - } + if (m->isPub) + regFn(handle + "." + s->Name + "." + m->Name, m.get()); } } else { - codegenCtx.registerPrivateName(handle, s->Name); + regPriv(s->Name); } } for (auto &e : importedModule->Enums) { if (e->isPub) aliasNamed(e->Name); - else codegenCtx.registerPrivateName(handle, e->Name); + else regPriv(e->Name); } for (auto &u : importedModule->Unions) { if (u->isPub) aliasNamed(u->Name); - else codegenCtx.registerPrivateName(handle, u->Name); + else regPriv(u->Name); } }; @@ -455,7 +466,7 @@ static int compileAndRun(const std::string &filename, if (import->Path == "test") continue; ModuleAST *importedModule = resolver.getOrLoadModule(import->Path); if (!importedModule) continue; - registerHandleFlats(import->Name, import->Path, importedModule); + registerHandleFlats(import->Name, import->Path, importedModule, ""); } // Destructured names that bind a re-exported module value — treat @@ -477,7 +488,43 @@ static int compileAndRun(const std::string &filename, auto resolved = resolveImportChain(re->Path, re->chain, resolveImportChain); if (!resolved.second) continue; - registerHandleFlats(name, resolved.first, resolved.second); + registerHandleFlats(name, resolved.first, resolved.second, ""); + } + } + + // Per-module import bindings for every OTHER loaded module, scoped to that + // module's path. So when an imported pub/cfn body is compiled later (under + // pushBodyModule(modPath) in the imported-body pass), its qualified imports + // (`std.fmt.print`) resolve against ITS module's imports, not the entry + // module's -- a body resolves names against its own declaring module. + // Mirrors the two entry-module passes above, but walks each loaded module's + // own Imports/DestructuringImports. + for (const auto &[modPath, loadedModule] : resolver.getLoadedModules()) { + for (auto &imp : loadedModule->Imports) { + if (imp->Path == "test") continue; + ModuleAST *target = resolver.getOrLoadModule(imp->Path); + if (!target) continue; + registerHandleFlats(imp->Name, imp->Path, target, modPath); + } + for (auto &destImp : loadedModule->DestructuringImports) { + if (destImp->Path == "test") continue; + ModuleAST *src = resolver.getOrLoadModule(destImp->Path); + if (!src) continue; + for (const auto &name : destImp->Names) { + const ImportDeclAST *re = nullptr; + for (auto &imp : src->Imports) { + if (imp->isPub && imp->Name == name) { + re = imp.get(); + break; + } + } + if (!re) continue; + auto resolved = + resolveImportChain(re->Path, re->chain, resolveImportChain); + if (!resolved.second) continue; + registerHandleFlats(name, resolved.first, resolved.second, + modPath); + } } } @@ -686,7 +733,12 @@ static int compileAndRun(const std::string &filename, auto registerStructMethods = [&](ModuleAST *m, bool publicOnly) -> int { for (auto &s : m->Structs) { for (auto &meth : s->Methods) { - if (publicOnly && !meth->isPub) continue; + // `cfn` methods (drop / default / at / setAt / len) are + // compiler-synthesized hooks that may be invoked from OTHER + // modules (e.g. `drop` fires at a scope exit in the importing + // module), so they must be declared + codegen'd even when not + // `pub`. Plain non-pub methods stay module-private. + if (publicOnly && !meth->isPub && !meth->isCfn) continue; // `cfn`-marked methods (drop / default / at / …) opt // in to compiler-synthesized calls and must match the // expected signature for their name. Plain `fn` @@ -751,6 +803,14 @@ static int compileAndRun(const std::string &filename, // bindings need their drop fn called at scope exit. jam::drops::DropRegistry dropRegistry = jam::drops::buildDropRegistry( *module, codegenCtx.getTypePool(), codegenCtx.getStringPool()); + // Fold imported modules' drops into the registry: a drop site in the main + // module must fire the destructor of a type defined and imported from + // another module (e.g. `Bus.drop` in bus.jam, dropped in main.jam). + for (const auto &[path, importedModule] : resolver.getLoadedModules()) { + jam::drops::addDropCandidates(dropRegistry, *importedModule, + codegenCtx.getTypePool(), + codegenCtx.getStringPool()); + } codegenCtx.setDropRegistry(&dropRegistry); // Pass 1d: every function (free fns + struct methods + imported @@ -805,6 +865,12 @@ static int compileAndRun(const std::string &filename, // not the caller's. Same mechanism the generic-instantiation // path uses — see JamCodegenContext::pushBodyModule. codegenCtx.pushBodyModule(path); + // Attribute diagnostics from this module's bodies to ITS file, not the + // entry file. The global NodeStore already carries the correct line; + // only currentFile() (used by locOf) is entry-global, so an error in an + // imported body must restore the right filename here. + std::string prevFile = codegenCtx.currentFile(); + codegenCtx.setCurrentFile(path + ".jam"); for (auto &func : importedModule->Functions) { if (func->isGeneric()) continue; if (func->isExtern) continue; @@ -823,7 +889,9 @@ static int compileAndRun(const std::string &filename, } for (auto &s : importedModule->Structs) { for (auto &m : s->Methods) { - if (!m->isPub) continue; + if (!m->isPub && !m->isCfn) + continue; // cfn hooks (drop/...) are invocable + // cross-module try { JirFunction jfn = astgenFunction(*m, codegenCtx); jfn.name = mangledFunctionName(*m, codegenCtx.getTypePool(), @@ -834,6 +902,7 @@ static int compileAndRun(const std::string &filename, } } } + codegenCtx.setCurrentFile(prevFile); codegenCtx.popBodyModule(); } diff --git a/tests/cpp/test_codegen_errors.cpp b/tests/cpp/test_codegen_errors.cpp index 1adc94a..1fb15ae 100644 --- a/tests/cpp/test_codegen_errors.cpp +++ b/tests/cpp/test_codegen_errors.cpp @@ -177,11 +177,17 @@ class CodegenErrorTests { framework.addTest("FnRef - ptr as u32 (narrower than u64) is rejected", testPtrAsNarrowIntRejected); framework.addTest( - "FnRef - u32 as *mut[] u8 (narrower than u64) is rejected", - testNarrowIntAsPtrRejected); + "FnRef - u32 as *mut[] u8 (int->ptr) zero-extends + inttoptr", + testNarrowIntAsPtrZeroExtends); framework.addTest( "FnRef - truly unknown variable still errors (no fn fallback)", testUnknownVariableStillErrors); + framework.addTest( + "XMod - error in imported body blames the DEFINING file", + testImportedBodyErrorBlamesDefiningFile); + framework.addTest( + "XMod - imported body cannot see the entry module's imports", + testImportedBodyCannotSeeEntryImports); } private: @@ -508,17 +514,24 @@ fn main() { ASSERT_TRUE(stderrContains(r, "unsupported `as` cast")); } - // Mirror of the above for the other direction: only u64 -> ptr - // is accepted (a u32 can't carry a full target pointer). - static void testNarrowIntAsPtrRejected() { - auto r = compileSource("narrow_int_as_ptr", R"( + // The OTHER direction is intentionally NOT symmetric: an int->ptr cast + // permits ANY integer width into a THIN pointer -- the int is an address, + // and narrower-than-pointer sources just widen to pointer width. (Only the + // reverse, ptr->int, is width-checked: u64 only.) So `u32 as *mut[] u8` is + // accepted and lowers to zext-to-i64 + inttoptr. `*mut[] u8` is a thin + // many-ptr, not a slice, so it is a legal target. + static void testNarrowIntAsPtrZeroExtends() { + auto r = compileSourceIR("narrow_int_as_ptr", R"( +extern fn sink(p: *mut[] u8); fn main() { var n: u32 = 0xFF; var p: *mut[] u8 = n as *mut[] u8; + sink(p); } )"); - ASSERT_TRUE(r.exitCode != 0); - ASSERT_TRUE(stderrContains(r, "unsupported `as` cast")); + ASSERT_TRUE(r.exitCode == 0); + ASSERT_TRUE(stderrContains(r, "inttoptr")); + ASSERT_TRUE(stderrContains(r, "zext")); } // Regression guard: the fn-name fallback in astgenVariable must @@ -536,6 +549,73 @@ fn main() { ASSERT_TRUE(stderrContains(r, "unknown variable")); ASSERT_TRUE(stderrContains(r, "nonexistent_thing")); } + + // An error inside an IMPORTED module's body (here, a `cfn drop` in + // lib.jam) must be attributed to lib.jam — the file the code was + // WRITTEN in — not to main.jam, the entry file being compiled. + // + // The imported-body codegen pass runs with the entry file as the + // global currentFile(); the fix sets currentFile() to the defining + // module for that pass. The node's line is already correct (one global + // NodeStore), so the bug was a wrong FILENAME with a right line. + static void testImportedBodyErrorBlamesDefiningFile() { + auto r = compileWithLib("xmod_body_err", + // main.jam: just imports + uses the type so + // lib.jam's drop body gets compiled. + R"( +const { makeBad } = import("lib"); +fn main() { + var b = makeBad(); +} +)", + // lib.jam: the bad reference is on line 4. + R"(pub const Bad = struct { + x: u32, + cfn drop(self: mut Self) { + self.x = unknownNameXyz; + } +}; +pub fn makeBad() Bad { return Bad { x: 0 }; } +)"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains(r, "unknown variable")); + // The whole point: the diagnostic names the DEFINING file... + ASSERT_TRUE(stderrContains(r, "lib.jam:4")); + // ...and never blames the entry file for this imported-body error. + ASSERT_TRUE(!stderrContains(r, "main.jam:")); + } + + // Import-scope strictness: a body resolves qualified names against ITS OWN + // module's imports, never the entry module's. Here main.jam binds + // `p = import("lib")`, but lib.jam's drop body uses `p.ping()` without + // importing `p` itself. It must be rejected (not silently resolved against + // main's `p`), and the error must point at lib.jam. + static void testImportedBodyCannotSeeEntryImports() { + auto r = compileWithLib("xmod_no_entry_leak", + R"( +const p = import("lib"); +const { makeThing } = import("lib"); +fn main() { + var n: u32 = 0; + var t: Thing = makeThing(&n); +} +)", + R"(pub fn ping() u32 { return 7; } +pub const Thing = struct { + sink: *mut u32, + cfn drop(self: mut Self) { + var q: *mut u32 = self.sink; + q.* = p.ping(); + } +}; +pub fn makeThing(s: *mut u32) Thing { return Thing { sink: s }; } +)"); + ASSERT_TRUE(r.exitCode != 0); + // `p` is undeclared in lib.jam's namespace -> rejected, not leaked. + ASSERT_TRUE(stderrContains(r, "unknown module handle")); + ASSERT_TRUE(stderrContains(r, "lib.jam:")); + ASSERT_TRUE(!stderrContains(r, "main.jam:")); + } }; int main() { diff --git a/tests/unit/test_drop_fn_call.jam b/tests/unit/test_drop_fn_call.jam new file mode 100644 index 0000000..ba16247 --- /dev/null +++ b/tests/unit/test_drop_fn_call.jam @@ -0,0 +1,68 @@ +// Does a `cfn drop` body actually RUN its statements -- specifically a +// function CALL with a side effect? That's the shape jamstation's Bus drop +// uses (discClose(...) + a print). test_drops already proves a direct pointer +// write inside drop runs; this isolates "drop body calls a function". + +const { assert } = import("test"); + +fn bumpThrough(p: *mut u32) { + var q: *mut u32 = p; + q.* = q.* + 1; +} + +const Thing = struct { + sink: *mut u32, + cfn drop(self: mut Self) { + bumpThrough(self.sink); + } +}; + +fn makeAndDrop(sink: *mut u32) { + var t: Thing = Thing { sink: sink }; + // t drops at scope exit -> bumpThrough(self.sink) must run once. +} + +const Two = struct { + sink: *mut u32, + cfn drop(self: mut Self) { + bumpThrough(self.sink); + bumpThrough(self.sink); + } +}; + +fn makeAndDropTwo(sink: *mut u32) { + var t: Two = Two { sink: sink }; +} + +tfn dropBodyRunsFunctionCall() { + var hits: u32 = 0; + makeAndDrop(&hits); + assert(hits, 1); +} + +tfn dropBodyRunsAllStatements() { + var hits: u32 = 0; + makeAndDropTwo(&hits); + assert(hits, 2); +} + +// The Bus scenario: a drop-bearing local passed by `let` borrow to functions +// (repeatedly), then dropped at the caller's scope exit. The borrow must NOT +// consume it -- the drop must still fire exactly ONCE at the end (not 0, not 2). +fn borrowThing(t: Thing) u32 { + var p: *mut u32 = t.sink; + return p.*; +} + +fn makeBorrowDrop(sink: *mut u32) { + var t: Thing = Thing { sink: sink }; + var a: u32 = borrowThing(t); + var b: u32 = borrowThing(t); + // t drops here -> bumpThrough(sink) must run exactly once. +} + +tfn dropFiresAfterLetBorrow() { + var hits: u32 = 0; + makeBorrowDrop(&hits); + assert(hits, 1); +} diff --git a/tests/unit/test_xmod_drop.jam b/tests/unit/test_xmod_drop.jam new file mode 100644 index 0000000..0797216 --- /dev/null +++ b/tests/unit/test_xmod_drop.jam @@ -0,0 +1,15 @@ +// A struct whose `cfn drop` is defined in ANOTHER module must fire its drop +// when dropped here. Regression for cross-module drop registration. +const { assert } = import("test"); +const { XThing, makeXThing } = import("test_xmod_drop_helper"); + +fn build(s: *mut u32) { + var t: XThing = makeXThing(s); + // t drops here -> XThing.drop (in the helper module) must run once. +} + +tfn crossModuleDropFires() { + var hits: u32 = 0; + build(&hits); + assert(hits, 1); +} diff --git a/tests/unit/test_xmod_drop_helper.jam b/tests/unit/test_xmod_drop_helper.jam new file mode 100644 index 0000000..d759bda --- /dev/null +++ b/tests/unit/test_xmod_drop_helper.jam @@ -0,0 +1,19 @@ +// Helper module for test_xmod_drop: a drop-bearing struct defined HERE, +// dropped in the importing test module. (No tfns -- it's a helper.) +// +// `lib` is imported HERE only. The entry test module does NOT import it, so +// the `cfn drop` body below can only compile if its qualified call resolves +// against THIS module's imports (the fix), not the entry module's. +const lib = import("test_xmod_lib"); + +pub const XThing = struct { + sink: *mut u32, + cfn drop(self: mut Self) { + var p: *mut u32 = self.sink; + p.* = lib.bumpBy(p.*, 1); + } +}; + +pub fn makeXThing(s: *mut u32) XThing { + return XThing { sink: s }; +} diff --git a/tests/unit/test_xmod_lib.jam b/tests/unit/test_xmod_lib.jam new file mode 100644 index 0000000..1c7551d --- /dev/null +++ b/tests/unit/test_xmod_lib.jam @@ -0,0 +1,7 @@ +// Tiny library imported BY test_xmod_drop_helper, but NOT by the test entry +// module (test_xmod_drop). Lets the cross-module-drop test prove that the +// helper's `cfn drop` body resolves its qualified imports against the helper's +// own module -- not the entry module's imports. +pub fn bumpBy(x: u32, n: u32) u32 { + return x + n; +}