diff --git a/src/abi.cpp b/src/abi.cpp index 0c76cc3..a1a57fe 100644 --- a/src/abi.cpp +++ b/src/abi.cpp @@ -13,9 +13,7 @@ namespace jam { namespace abi { bool isByRef(TypeIdx ty, const JamCodegenContext &ctx) { - if (ty == kNoType) { - return false; - } + if (ty == kNoType) { return false; } const TypeKey &k = ctx.getTypePool().get(ty); switch (k.kind) { diff --git a/src/ast.h b/src/ast.h index 6d82ff8..b235ff9 100644 --- a/src/ast.h +++ b/src/ast.h @@ -138,6 +138,12 @@ class StructDeclAST { std::vector> Fields; // (name, type) std::vector> Methods; bool isPub = false; + // Owning module's entry-relative identity (e.g. "lib/b"); empty for + // the entry module. Stamped post-parse by the module resolver and + // used to give same-named types in different modules distinct + // qualified identities (`lib/b.Thing`), mirroring how functions are + // qualified by `FunctionAST::modulePath`. + std::string modulePath; StructDeclAST(std::string Name, std::vector> Fields, @@ -175,6 +181,9 @@ class EnumDeclAST { std::string Name; std::vector Variants; bool isPub = false; + // Owning module's entry-relative identity; empty for the entry + // module. See StructDeclAST::modulePath. + std::string modulePath; EnumDeclAST(std::string Name, std::vector Variants) : Name(std::move(Name)), Variants(std::move(Variants)) {} @@ -197,6 +206,9 @@ class UnionDeclAST { std::string Name; std::vector> Fields; // (name, type) bool isPub = false; + // Owning module's entry-relative identity; empty for the entry + // module. See StructDeclAST::modulePath. + std::string modulePath; UnionDeclAST(std::string Name, std::vector> Fields) @@ -227,6 +239,9 @@ class ConstDeclAST { // plain const with a non-foldable init only errors when a comp // position consumes it; a comp const errors at the declaration). bool isComp = false; + // Owning module's entry-relative identity; empty for the entry + // module. See StructDeclAST::modulePath. + std::string modulePath; ConstDeclAST(std::string Name, TypeIdx DeclaredType, NodeIdx InitExpr) : Name(std::move(Name)), DeclaredType(DeclaredType), diff --git a/src/ast_flat.h b/src/ast_flat.h index 60fc1b0..23a05e6 100644 --- a/src/ast_flat.h +++ b/src/ast_flat.h @@ -533,7 +533,15 @@ class TypePool { return pushKey(k); } - const TypeKey &get(TypeIdx i) const { return keys_[i]; } + // Returned BY VALUE deliberately: interning can reallocate `keys_`, + // and resolution code routinely interns (module-qualified names, + // instantiation results) while still holding the result of an + // earlier get(). A reference would dangle and read freed memory — + // this fired in practice as an "unsupported `as` cast" whose + // destination TypeKey read as Invalid. The 12-byte copy is free; + // existing `const TypeKey &k = pool.get(i)` callers bind the + // temporary with extended lifetime. + TypeKey get(TypeIdx i) const { return keys_[i]; } std::size_t size() const { return keys_.size(); } // Convenience constructors that intern in one call. diff --git a/src/astgen.cpp b/src/astgen.cpp index e70f7a8..913c1f1 100644 --- a/src/astgen.cpp +++ b/src/astgen.cpp @@ -599,12 +599,36 @@ static JirRef astgenNumberLit(AstGenCtx &gctx, const AstNode &n, // transitively. Otherwise fall back to the smallest-fit width. if (expected != kNoType) { TypeIdx resolved = expected; - const TypeKey &k0 = gctx.ctx.getTypePool().get(resolved); - if (k0.kind == TypeKind::GenericCall) { - TypeIdx r = gctx.ctx.resolveGenericCall(resolved); - if (r != kNoType) resolved = r; + { + const TypeKey k0 = gctx.ctx.getTypePool().get(resolved); + if (k0.kind == TypeKind::GenericCall) { + TypeIdx r = gctx.ctx.resolveGenericCall(resolved); + if (r != kNoType) resolved = r; + } + } + // A Named expected may be a type-alias chain ending at an + // integer (`const Flag = u64; var f: Flag = 100000;`). Chase it + // — requalifying first so the alias resolves in the right + // module — the same way the init-mismatch check's resolveForCmp + // does, so the literal settles at the alias's width instead of + // smallest-fit (which would then fail that very check). + for (int hop = 0; hop < 8; hop++) { + TypeIdx rq = + gctx.ctx.requalifyType(resolved, gctx.ctx.currentBodyModule()); + const TypeKey kn = gctx.ctx.getTypePool().get(rq); + if (kn.kind != TypeKind::Named) { + resolved = rq; + break; + } + TypeIdx target = gctx.ctx.lookupTypeAlias( + gctx.ctx.getStringPool().get(static_cast(kn.a))); + if (target == kNoType || target == rq) { + resolved = rq; + break; + } + resolved = target; } - const TypeKey &k = gctx.ctx.getTypePool().get(resolved); + const TypeKey k = gctx.ctx.getTypePool().get(resolved); if (k.kind == TypeKind::Int) { inst.ty = resolved; return emit(gctx, inst); @@ -1042,6 +1066,10 @@ static void astgenReturn(AstGenCtx &gctx, const AstNode &n) { // instantiated-drops table so generic struct/enum instantiations // (Vec(i32), Holder(i32), ...) fire drops too. static std::string lookupDropFnLLVMName(JamCodegenContext &ctx, TypeIdx ty) { + // Qualify a bare body-level type to its owning module so the drop + // registry (keyed by the qualified self-param type) hits for a type + // whose `cfn drop` lives in another module. + ty = ctx.requalifyType(ty, ctx.currentBodyModule()); const TypeKey &k = ctx.getTypePool().get(ty); std::string typeName; if (k.kind == TypeKind::Struct || k.kind == TypeKind::Named || @@ -1223,6 +1251,11 @@ static void astgenVarDecl(AstGenCtx &gctx, const AstNode &n) { std::function resolveForCmp = [&](TypeIdx t) -> TypeIdx { if (t == kNoType) return t; + // Qualify bare user-type references first so a declared + // bare `Color` and an initializer typed with the + // qualified `mod.Color` compare equal. No-op for + // already-qualified, substitution, and primitive types. + t = gctx.ctx.requalifyType(t, gctx.ctx.currentBodyModule()); const TypeKey &k = gctx.ctx.getTypePool().get(t); // Generic substitution wins (inside an instantiated // method body, `T` resolves to whatever the @@ -2011,14 +2044,56 @@ static JirRef astgenMemberAccess(AstGenCtx &gctx, const AstNode &n) { const std::string &member = gctx.ctx.getStringPool().get(memberId); const AstNode &baseNode = ns.get(baseIdx); + // The enum receiver may be a bare Variable (`Color.Red`) or a + // handle-qualified chain (`a.Status.Ok` — base is the MemberAccess + // `a.Status`). Stringify pure Variable/MemberAccess chains so both + // spellings resolve through the same requalifying enum lookup. + std::string baseName; if (baseNode.tag == AstTag::Variable) { - const std::string &baseName = + baseName = gctx.ctx.getStringPool().get(static_cast(baseNode.lhs)); + } else if (baseNode.tag == AstTag::MemberAccess) { + std::vector segs; + NodeIdx cur = baseIdx; + while (true) { + const AstNode &c = ns.get(cur); + if (c.tag == AstTag::MemberAccess) { + segs.push_back(gctx.ctx.getStringPool().get( + static_cast(c.rhs))); + cur = static_cast(c.lhs); + continue; + } + if (c.tag == AstTag::Variable) { + segs.push_back(gctx.ctx.getStringPool().get( + static_cast(c.lhs))); + break; + } + segs.clear(); // not a pure name chain (index, call, ...) + break; + } + // Only treat the chain as a possible enum receiver when its + // ROOT is not a local — `self.field.member` stays a projection. + if (!segs.empty() && + gctx.locals.find(segs.back()) == gctx.locals.end()) { + for (auto it = segs.rbegin(); it != segs.rend(); ++it) { + if (!baseName.empty()) baseName += "."; + baseName += *it; + } + } + } + if (!baseName.empty()) { // Enum-variant unit reference: emit JirTag::StructLit with a // single field (the tag) for payloaded enums; for unit-only // enums, the tag value IS the runtime form (i8). - if (const auto *einfo = gctx.ctx.getEnum(baseName)) { - int vidx = gctx.ctx.getEnumVariantIndex(baseName, member); + // Resolve the source name through lookupEnum (which requalifies + // against the current body module and follows handle aliases / + // re-export chains) so an enum owned by an imported module — + // registered under its qualified identity — is found from its + // own bodies, importers, and handle-qualified spellings. + TypeIdx baseTy = gctx.ctx.getTypePool().internNamed( + gctx.ctx.getStringPool().intern(baseName)); + if (const auto *einfo = gctx.ctx.lookupEnum(baseTy)) { + int vidx = gctx.ctx.getEnumVariantIndex(einfo->name, member); if (vidx < 0) { failHere(gctx, "astgen: enum `" + baseName + "` has no variant `" + member + "`"); @@ -2027,14 +2102,24 @@ static JirRef astgenMemberAccess(AstGenCtx &gctx, const AstNode &n) { TypeIdx enumTy = gctx.ctx.getTypePool().intern( TypeKey{TypeKind::Named, 0, 0, static_cast( - gctx.ctx.getStringPool().intern(baseName)), + gctx.ctx.getStringPool().intern(einfo->name)), 0}); JirInst tag{}; tag.tag = JirTag::Int; tag.a = disc; tag.ty = BuiltinType::U8; JirRef tagRef = emit(gctx, tag); - if (!einfo->hasPayloadVariant) { return tagRef; } + if (!einfo->hasPayloadVariant) { + // Unit-only enums lower to i8, but the VALUE's type is + // the enum — retag via BitCast (same shape the match + // lowering uses) so `var d: Color = Color.Blue;` + // type-checks against the declared enum type. + JirInst cast{}; + cast.tag = JirTag::BitCast; + cast.a = tagRef; + cast.ty = enumTy; + return emit(gctx, cast); + } // Payloaded enum: build a {tag, payload-undef} struct. // We carry the enum's TypeIdx so codegen materialises the // right struct shape. @@ -3871,8 +3956,17 @@ static void astgenPatternCompare(AstGenCtx &gctx, NodeIdx patIdx, JirRef scrut, } else { std::string recvName = gctx.ctx.getStringPool().get(static_cast(recvSlot)); - enumName = recvName; - einfo = gctx.ctx.getEnum(recvName); + // Resolve through lookupEnum so a bare receiver naming an + // enum owned by another module requalifies to its + // registered identity. + TypeIdx recvTy = gctx.ctx.getTypePool().internNamed( + gctx.ctx.getStringPool().intern(recvName)); + if (const auto *info = gctx.ctx.lookupEnum(recvTy)) { + enumName = info->name; + einfo = info; + } else { + enumName = recvName; + } } if (einfo == nullptr) { // Not an enum. A bare-identifier pattern (infer-receiver, no @@ -4584,9 +4678,10 @@ bool typeNeedsDropInner(JamCodegenContext &ctx, TypeIdx ty) { if (tk.kind != TypeKind::Struct && tk.kind != TypeKind::Named) { return false; } - const std::string &name = - ctx.getStringPool().get(static_cast(tk.a)); - const auto *sinfo = ctx.getStruct(name); + // lookupStruct requalifies a bare name against the current body + // module, so imported structs (registered under their qualified + // identity) classify their drop-bearing fields correctly here. + const auto *sinfo = ctx.lookupStruct(ty); if (sinfo == nullptr) return false; for (const auto &f : sinfo->fields) { if (typeNeedsDrop(ctx, f.second)) return true; @@ -4673,9 +4768,8 @@ static void emitFieldDrops(AstGenCtx &gctx, JirRef ptrRef, TypeIdx pointeeTy) { const TypeKey &tk = gctx.ctx.getTypePool().get(pointeeTy); const JamCodegenContext::StructInfo *sinfo = nullptr; if (tk.kind == TypeKind::Struct || tk.kind == TypeKind::Named) { - std::string name = - gctx.ctx.getStringPool().get(static_cast(tk.a)); - sinfo = gctx.ctx.getStruct(name); + // Requalifying lookup — see typeNeedsDropInner. + sinfo = gctx.ctx.lookupStruct(pointeeTy); } if (sinfo == nullptr) return; for (size_t i = 0; i < sinfo->fields.size(); i++) { @@ -5701,8 +5795,13 @@ static JirRef lowerArgInner(AstGenCtx &gctx, NodeIdx argIdx, const Param &p) { if (gctx.locals.find(name) != gctx.locals.end()) return false; // Not a local — likely an enum / module / type name. // Treat as non-lvalueable so the spill path takes over. - return gctx.ctx.getEnum(name) != nullptr || - gctx.ctx.getStruct(name) != nullptr || + // lookupEnum/lookupStruct requalify the bare name against the + // current body module, so enums/structs owned by an imported + // module are detected too. + TypeIdx namedTy = gctx.ctx.getTypePool().internNamed( + gctx.ctx.getStringPool().intern(name)); + return gctx.ctx.lookupEnum(namedTy) != nullptr || + gctx.ctx.lookupStruct(namedTy) != nullptr || gctx.ctx.getImportHandle(name) != nullptr; }; switch (argNode.tag) { @@ -6453,7 +6552,14 @@ static JirRef astgenCompInstantiatedCall(AstGenCtx &gctx, const AstNode &n, } } - std::string instName = fn->Name + mangleSuffix; + // The clone's name doubles as the instantiation-cache key AND the + // LLVM symbol (the clone's modulePath stays empty, so mangling + // passes the name through). Prefix with the callee's qualified + // identity: two modules' same-named comp fns must neither share a + // cache slot (wrong clone dispatched) nor an LLVM symbol (linker + // silently merges them). + std::string instName = + qualifyTypeName(fn->modulePath, fn->Name) + mangleSuffix; // Cache hit? Skip the clone+lower and dispatch to the existing // instantiation. @@ -6512,6 +6618,115 @@ static JirRef astgenCompInstantiatedCall(AstGenCtx &gctx, const AstNode &n, return emitCall(gctx, clone, argRefs); } +// Construct `Enum.Variant(args...)`: the tag value for unit variants, +// an alloca + tag/payload stores for payloaded ones. `canonicalType` +// is the enum's registered (qualified) identity. Returns kNoJirRef +// when `variantName` is not a variant of `einfo` — callers fall +// through to method dispatch. Shared by the single-dot +// (`Result.Ok(x)`) and handle-qualified (`a.Status.Bad(5)`) call +// paths. +static JirRef astgenEnumVariantCtor( + AstGenCtx &gctx, const JamCodegenContext::EnumInfo *enumForVariant, + const std::string &canonicalType, const std::string &methodName, + ExtraIdx argsExtra, uint32_t argCount) { + const NodeStore &ns = gctx.ctx.getNodeStore(); + int vidx = gctx.ctx.getEnumVariantIndex(canonicalType, methodName); + if (vidx < 0) return kNoJirRef; + const auto &variant = enumForVariant->variants[vidx]; + TypeIdx enumTy = gctx.ctx.getTypePool().intern(TypeKey{ + TypeKind::Named, 0, 0, + static_cast(gctx.ctx.getStringPool().intern(canonicalType)), + 0}); + JirInst tag{}; + tag.tag = JirTag::Int; + tag.a = static_cast(variant.discriminant); + tag.ty = BuiltinType::U8; + JirRef tagRef = emit(gctx, tag); + if (!enumForVariant->hasPayloadVariant) { return tagRef; } + // Build {tag, payload-undef|val} via alloca + FieldAddr stores, + // then load. Mirrors the TypeMethodCall path. + JirInst alloca{}; + alloca.tag = JirTag::Alloca; + alloca.ty = enumTy; + JirRef slot = emitAllocaHoisted(gctx, alloca); + TypeIdx u8PtrTy = gctx.ctx.getTypePool().intern( + TypeKey{TypeKind::PtrSingle, 0, 0, BuiltinType::U8, 0}); + JirInst tagFA{}; + tagFA.tag = JirTag::FieldAddr; + tagFA.a = slot; + tagFA.b = 0; + tagFA.ty = u8PtrTy; + JirRef tagPtr = emit(gctx, tagFA); + JirInst tagStore{}; + tagStore.tag = JirTag::Store; + tagStore.a = tagPtr; + tagStore.b = tagRef; + emit(gctx, tagStore); + // Store every payload field. Each one lives at its own byte offset + // within the payload area (field 1 of the enum struct is the + // alignment driver, which holds the first payload; subsequent + // payloads spill into the `extraBytes` array — see codegen.cpp's + // enum layout). We address them all uniformly via a payload-area + // pointer + byte offset GEP through i8. + if (argCount > variant.payloadTypes.size()) { + failHere(gctx, "astgen: too many args for variant `" + canonicalType + + "." + methodName + "`"); + } + if (!variant.payloadTypes.empty() && argCount >= 1) { + TypeIdx payAreaPtrTy = gctx.ctx.getTypePool().intern( + TypeKey{TypeKind::PtrSingle, 0, 0, BuiltinType::U8, 0}); + JirInst payAreaFA{}; + payAreaFA.tag = JirTag::FieldAddr; + payAreaFA.a = slot; + payAreaFA.b = 1; + payAreaFA.ty = payAreaPtrTy; + JirRef payAreaPtr = emit(gctx, payAreaFA); + + // Byte-stride GEP through `payAreaPtr` (which is *u8) gives the + // per-field pointer. Encode the result type as `*u8` so + // jir_codegen's IndexAddr uses i8 stride (1 byte per `idx` + // step); opaque pointers let us Store the field's actual type + // to the resulting ptr without a separate cast. + uint64_t off = 0; + for (uint32_t i = 0; i < variant.payloadTypes.size() && i < argCount; + i++) { + TypeIdx fieldTy = variant.payloadTypes[i]; + uint64_t s = gctx.ctx.typeSize(fieldTy); + uint64_t a = gctx.ctx.typeAlign(fieldTy); + off = (off + a - 1) / a * a; + NodeIdx argIdxN = + static_cast(ns.getExtra(argsExtra + 1 + i)); + JirRef payVal = astgenExpr(gctx, argIdxN, fieldTy); + // Enum payload capture is a MOVE — the enum owns the value + // (see the TypeMethodCall constructor for the same rule). + rejectDropBearingFieldExtract(gctx, argIdxN, fieldTy, "capture"); + consumeMovedVariable(gctx, argIdxN); + JirInst gepInst{}; + gepInst.tag = JirTag::IndexAddr; + gepInst.a = payAreaPtr; + JirInst offC{}; + offC.tag = JirTag::Int; + offC.a = static_cast(off); + offC.ty = BuiltinType::U64; + JirRef offRef = emit(gctx, offC); + gepInst.b = offRef; + gepInst.ty = payAreaPtrTy; // *u8 -> byte stride + JirRef fieldPtr = emit(gctx, gepInst); + JirInst payStore{}; + payStore.tag = JirTag::Store; + payStore.a = fieldPtr; + payStore.b = payVal; + emit(gctx, payStore); + off += s; + } + } + JirInst load{}; + load.tag = JirTag::Load; + load.a = slot; + load.ty = enumTy; + return emit(gctx, load); +} + static JirRef astgenCall(AstGenCtx &gctx, const AstNode &n, JirRef destPtr) { const NodeStore &ns = gctx.ctx.getNodeStore(); ExtraIdx argsExtra = static_cast(n.rhs); @@ -6738,7 +6953,24 @@ static JirRef astgenCall(AstGenCtx &gctx, const AstNode &n, JirRef destPtr) { // importer's namespace handle, not in a flat global table. The // registration site in main.cpp puts these under the key // `handle.Struct.method`; we look them up directly here. + // + // Handle-qualified ENUM variants (`a.Status.Bad(5)`) take the same + // spelling: split at the LAST dot, resolve the prefix as an enum + // through the requalifying lookup (which follows handle aliases and + // re-export chains), and construct the variant. if (callee.find('.') != callee.rfind('.')) { + { + size_t lastDot = callee.rfind('.'); + std::string recvName = callee.substr(0, lastDot); + std::string variantName = callee.substr(lastDot + 1); + TypeIdx recvTy = gctx.ctx.getTypePool().internNamed( + gctx.ctx.getStringPool().intern(recvName)); + if (const auto *einfo = gctx.ctx.lookupEnum(recvTy)) { + JirRef built = astgenEnumVariantCtor( + gctx, einfo, einfo->name, variantName, argsExtra, argCount); + if (built != kNoJirRef) return built; + } + } if (const FunctionAST *method = gctx.ctx.getFunctionAST(callee)) { std::vector argRefs; argRefs.reserve(argCount); @@ -6787,10 +7019,15 @@ static JirRef astgenCall(AstGenCtx &gctx, const AstNode &n, JirRef destPtr) { } } if (canonicalType.empty()) { - if (const auto *sinfo = gctx.ctx.getStruct(resolvedPrefix)) { + // Qualify a bare type prefix (e.g. a destructuring- + // imported `File`) to its owning module before the + // registry lookup, via lookupStruct/lookupEnum which + // requalify against the current body module. + TypeIdx prefixTy = gctx.ctx.getTypePool().internNamed( + gctx.ctx.getStringPool().intern(resolvedPrefix)); + if (const auto *sinfo = gctx.ctx.lookupStruct(prefixTy)) { canonicalType = sinfo->name; - } else if (const auto *einfo = - gctx.ctx.getEnum(resolvedPrefix)) { + } else if (const auto *einfo = gctx.ctx.lookupEnum(prefixTy)) { canonicalType = einfo->name; enumForVariant = einfo; } @@ -6798,111 +7035,10 @@ static JirRef astgenCall(AstGenCtx &gctx, const AstNode &n, JirRef destPtr) { // Enum-variant constructor (`Result.Ok(x)`-style Call): // the suffix is a variant name on the resolved enum. if (enumForVariant != nullptr) { - int vidx = - gctx.ctx.getEnumVariantIndex(canonicalType, methodName); - if (vidx >= 0) { - const auto &variant = enumForVariant->variants[vidx]; - TypeIdx enumTy = gctx.ctx.getTypePool().intern(TypeKey{ - TypeKind::Named, 0, 0, - static_cast( - gctx.ctx.getStringPool().intern(canonicalType)), - 0}); - JirInst tag{}; - tag.tag = JirTag::Int; - tag.a = static_cast(variant.discriminant); - tag.ty = BuiltinType::U8; - JirRef tagRef = emit(gctx, tag); - if (!enumForVariant->hasPayloadVariant) { return tagRef; } - // Build {tag, payload-undef|val} via alloca + FieldAddr - // stores, then load. Mirrors the TypeMethodCall path. - JirInst alloca{}; - alloca.tag = JirTag::Alloca; - alloca.ty = enumTy; - JirRef slot = emitAllocaHoisted(gctx, alloca); - TypeIdx u8PtrTy = gctx.ctx.getTypePool().intern( - TypeKey{TypeKind::PtrSingle, 0, 0, BuiltinType::U8, 0}); - JirInst tagFA{}; - tagFA.tag = JirTag::FieldAddr; - tagFA.a = slot; - tagFA.b = 0; - tagFA.ty = u8PtrTy; - JirRef tagPtr = emit(gctx, tagFA); - JirInst tagStore{}; - tagStore.tag = JirTag::Store; - tagStore.a = tagPtr; - tagStore.b = tagRef; - emit(gctx, tagStore); - // Store every payload field. Each one lives at its own - // byte offset within the payload area (field 1 of the - // enum struct is the alignment driver, which holds the - // first payload; subsequent payloads spill into the - // `extraBytes` array — see codegen.cpp's enum layout). - // We address them all uniformly via a payload-area - // pointer + byte offset GEP through i8. - if (argCount > variant.payloadTypes.size()) { - failHere(gctx, "astgen: too many args for variant `" + - canonicalType + "." + methodName + - "`"); - } - if (!variant.payloadTypes.empty() && argCount >= 1) { - TypeIdx payAreaPtrTy = - gctx.ctx.getTypePool().intern(TypeKey{ - TypeKind::PtrSingle, 0, 0, BuiltinType::U8, 0}); - JirInst payAreaFA{}; - payAreaFA.tag = JirTag::FieldAddr; - payAreaFA.a = slot; - payAreaFA.b = 1; - payAreaFA.ty = payAreaPtrTy; - JirRef payAreaPtr = emit(gctx, payAreaFA); - - // Byte-stride GEP through `payAreaPtr` (which is *u8) - // gives the per-field pointer. Encode the result type - // as `*u8` so jir_codegen's IndexAddr uses i8 stride - // (1 byte per `idx` step); opaque pointers let us - // Store the field's actual type to the resulting ptr - // without a separate cast. - uint64_t off = 0; - for (uint32_t i = 0; - i < variant.payloadTypes.size() && i < argCount; - i++) { - TypeIdx fieldTy = variant.payloadTypes[i]; - uint64_t s = gctx.ctx.typeSize(fieldTy); - uint64_t a = gctx.ctx.typeAlign(fieldTy); - off = (off + a - 1) / a * a; - NodeIdx argIdxN = static_cast( - ns.getExtra(argsExtra + 1 + i)); - JirRef payVal = astgenExpr(gctx, argIdxN, fieldTy); - // Enum payload capture is a MOVE — the enum - // owns the value (see the TypeMethodCall - // constructor for the same rule). - rejectDropBearingFieldExtract(gctx, argIdxN, - fieldTy, "capture"); - consumeMovedVariable(gctx, argIdxN); - JirInst gepInst{}; - gepInst.tag = JirTag::IndexAddr; - gepInst.a = payAreaPtr; - JirInst offC{}; - offC.tag = JirTag::Int; - offC.a = static_cast(off); - offC.ty = BuiltinType::U64; - JirRef offRef = emit(gctx, offC); - gepInst.b = offRef; - gepInst.ty = payAreaPtrTy; // *u8 -> byte stride - JirRef fieldPtr = emit(gctx, gepInst); - JirInst payStore{}; - payStore.tag = JirTag::Store; - payStore.a = fieldPtr; - payStore.b = payVal; - emit(gctx, payStore); - off += s; - } - } - JirInst load{}; - load.tag = JirTag::Load; - load.a = slot; - load.ty = enumTy; - return emit(gctx, load); - } + JirRef built = + astgenEnumVariantCtor(gctx, enumForVariant, canonicalType, + methodName, argsExtra, argCount); + if (built != kNoJirRef) return built; } if (!canonicalType.empty()) { std::string qualified = canonicalType + "." + methodName; @@ -7502,17 +7638,22 @@ bool typeNeedsDrop(JamCodegenContext &ctx, TypeIdx ty) { } JirFunction astgenMetadata(const FunctionAST &fn, JamCodegenContext &ctx) { - (void)ctx; JirFunction jfn; jfn.name = fn.Name; - jfn.returnType = fn.ReturnType; + jfn.modulePath = fn.modulePath; + // Qualify the signature's user-type references to the function's + // owning module, so a prototype emitted with no body-module on the + // stack (the imported-prototype passes in main.cpp) still resolves + // `Thing` to `.Thing`. Mirrors how bodies qualify via + // currentBodyModule; here the module comes straight from the AST. + jfn.returnType = ctx.requalifyType(fn.ReturnType, fn.modulePath); jfn.isExtern = fn.isExtern; jfn.isExport = fn.isExport; jfn.isPub = fn.isPub; jfn.isTest = fn.isTest; jfn.isVarArgs = fn.isVarArgs; for (const Param &p : fn.Args) { - jfn.paramTypes.push_back(p.Type); + jfn.paramTypes.push_back(ctx.requalifyType(p.Type, fn.modulePath)); jfn.paramModes.push_back(p.Mode); } return jfn; diff --git a/src/codegen.cpp b/src/codegen.cpp index b8296ce..19f13e0 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -92,6 +92,14 @@ TypeIdx JamCodegenContext::internFromString(const std::string &typeStr) const { } JamTypeRef JamCodegenContext::getLLVMType(TypeIdx ty) const { + // Resolve a bare user-type reference to its module-qualified TypeIdx + // before anything else, so the per-TypeIdx LLVM-type cache below keys + // each module's same-named type separately. Declaration-level types + // (struct fields, etc.) are already qualified at registration, so + // this only fires for body-level references, where currentBodyModule + // names the type's owning module. Qualified / primitive types are + // returned unchanged, so this is a cheap no-op in the common case. + ty = requalifyType(ty, currentBodyModule()); if (ty >= llvmTypeCache.size()) { llvmTypeCache.resize(ty + 1, nullptr); } if (llvmTypeCache[ty]) return llvmTypeCache[ty]; @@ -274,6 +282,10 @@ void JamCodegenContext::registerStruct( const JamCodegenContext::StructInfo * JamCodegenContext::lookupStruct(TypeIdx ty) const { if (ty == kNoType) return nullptr; + // Qualify a bare body-level reference to its owning module so the + // registry lookup below hits this module's struct, not a same-named + // one elsewhere. No-op for already-qualified / non-user types. + ty = requalifyType(ty, currentBodyModule()); const TypeKey &k = typePool.get(ty); // a `GenericCall` TypeIdx resolves to a concrete type // (typically a Named struct produced by instantiation). Recurse on @@ -322,6 +334,104 @@ int JamCodegenContext::getFieldIndex(const std::string &structName, return -1; } +void JamCodegenContext::registerTypeOwner(const std::string &ctxModule, + const std::string &typeName, + const std::string &ownerModule) { + typeModuleOf_[ctxModule][typeName] = ownerModule; +} + +TypeIdx JamCodegenContext::requalifyType(TypeIdx ty, + const std::string &ctxModule) const { + if (ty == kNoType) return ty; + // By value, not by reference: the recursive calls below intern new + // types, which can reallocate the TypePool's storage and dangle a + // reference into it. + TypeKey k = typePool.get(ty); + switch (k.kind) { + case TypeKind::Named: + case TypeKind::Struct: + case TypeKind::Enum: + case TypeKind::Union: { + const std::string &name = stringPool.get(static_cast(k.a)); + // Already qualified / handle.Type / chained: leave it for the + // alias and chained-resolution paths in lookupStruct. + if (name.find('.') != std::string::npos) return ty; + // An active generic-parameter substitution (T, Self) wins over a + // same-named module-level type: the body meant the parameter. + if (lookupCurrentSubst(name) != kNoType) return ty; + auto mIt = typeModuleOf_.find(ctxModule); + if (mIt == typeModuleOf_.end()) return ty; + auto tIt = mIt->second.find(name); + if (tIt == mIt->second.end()) return ty; // std / generic param / Self + std::string q = qualifyTypeName(tIt->second, name); + if (q == name) return ty; // owner is the entry module — stays bare + TypeKey nk = k; + nk.a = static_cast(stringPool.intern(q)); + return typePool.intern(nk); + } + case TypeKind::PtrSingle: + case TypeKind::PtrMany: + case TypeKind::Slice: + case TypeKind::Array: + case TypeKind::ArrayExpr: { + // Wrapper kinds carry the element TypeIdx in `a`; `b` (length / + // length-expr) is preserved untouched. + TypeIdx elem = requalifyType(static_cast(k.a), ctxModule); + if (elem == static_cast(k.a)) return ty; + TypeKey nk = k; + nk.a = static_cast(elem); + return typePool.intern(nk); + } + case TypeKind::GenericCall: { + // Qualify BOTH the callee and the type arguments. The callee + // (`Vec`) qualifies to its owning module's identity + // (`std/collections.Vec`) exactly like a concrete type name — + // two modules' same-named generic factories must never share a + // GenericCall TypeIdx, or the per-TypeIdx resolution cache hands + // one module the other's instantiation. Args qualify so the + // instantiation substitutes the right concrete types + // (`Option(File)` -> `Option(std/fs.File)`). + // + // Copy the name and args BEFORE recursing: the recursive + // requalifyType calls below may intern new types, reallocating + // the TypePool's internal vectors and invalidating any reference + // into them (`k` and the args span). + StringIdx nameId = static_cast(k.a); + std::vector args = + typePool.genericArgsAt(static_cast(k.b)); + bool changed = false; + { + const std::string &callee = stringPool.get(nameId); + if (callee.find('.') == std::string::npos && + lookupCurrentSubst(callee) == kNoType) { + auto mIt = typeModuleOf_.find(ctxModule); + if (mIt != typeModuleOf_.end()) { + auto tIt = mIt->second.find(callee); + if (tIt != mIt->second.end()) { + std::string q = qualifyTypeName(tIt->second, callee); + if (q != callee) { + nameId = stringPool.intern(q); + changed = true; + } + } + } + } + } + std::vector newArgs; + newArgs.reserve(args.size()); + for (TypeIdx a : args) { + TypeIdx r = requalifyType(a, ctxModule); + if (r != a) changed = true; + newArgs.push_back(r); + } + if (!changed) return ty; + return typePool.internGenericCall(nameId, std::move(newArgs)); + } + default: + return ty; + } +} + // Union registry void JamCodegenContext::registerUnion( @@ -344,6 +454,7 @@ JamCodegenContext::getUnion(const std::string &name) const { const JamCodegenContext::UnionInfo * JamCodegenContext::lookupUnion(TypeIdx ty) const { if (ty == kNoType) return nullptr; + ty = requalifyType(ty, currentBodyModule()); const TypeKey &k = typePool.get(ty); // Accept TypeKind::Union (explicit) or TypeKind::Named (parser- // deferred user type that resolves to a union). @@ -411,6 +522,7 @@ JamCodegenContext::getEnum(const std::string &name) const { const JamCodegenContext::EnumInfo * JamCodegenContext::lookupEnum(TypeIdx ty) const { if (ty == kNoType) return nullptr; + ty = requalifyType(ty, currentBodyModule()); const TypeKey &k = typePool.get(ty); // a GenericCall TypeIdx resolves to a concrete type; // recurse on the resolved TypeIdx so generic enum instantiations @@ -457,27 +569,31 @@ JamCodegenContext::findEnumByLLVMType(JamTypeRef ty) const { } void JamCodegenContext::registerModuleConst(const std::string &name, + const std::string &bareName, + const std::string &owner, NodeIdx init, TypeIdx declared, bool isComp, std::string file) { - moduleConsts[name] = - ModuleConstInfo{init, declared, isComp, std::move(file)}; + moduleConsts[name] = ModuleConstInfo{ + init, declared, isComp, std::move(file), bareName, owner}; } void JamCodegenContext::validateCompConsts() { jam::ComptimeEvaluator ev(nodeStore, stringPool, typePool); - jam::ComptimeScope scope; - seedComptimeScope(scope); for (const auto &kv : moduleConsts) { if (!kv.second.isComp) continue; - // A foldable comp const is already bound in the seeded scope; - // re-evaluating its init there is cheap and gives a definitive - // verdict for the ones the fixpoint never managed to bind. + // Evaluate each comp const against ITS OWN module's scope — the + // initializer may reference sibling consts by bare name, and + // those must be the owner's, not another module's. + pushBodyModule(kv.second.owner); + jam::ComptimeScope scope; + seedComptimeScope(scope); jam::ComptimeValue v = ev.eval(kv.second.initExpr, scope); + popBodyModule(); if (v.isNone()) { jam::SrcLoc loc{kv.second.file, nodeStore.getLine(kv.second.initExpr)}; diagnostics().error( - loc, "comp const `" + kv.first + + loc, "comp const `" + kv.second.bareName + "` must be compile-time evaluable — its " "initializer depends on a runtime value or an " "unsupported construct"); @@ -629,7 +745,15 @@ TypeIdx JamCodegenContext::resolveChainedType(const std::string &dotted) const { auto r = walkChain(*this, dotted); if (!r.leaf) return kNoType; auto tit = r.leaf->types.find(r.lastSeg); - return (tit == r.leaf->types.end()) ? kNoType : tit->second; + if (tit == r.leaf->types.end()) return kNoType; + // No-progress guard: when an import handle shares its name with the + // module's identity (`const m = import("m")`), the chain resolves + // `m.Inner` to the SAME qualified Named that was asked about. Every + // caller tail-recurses on the result, so report "nothing further to + // resolve" instead of handing back the input. + TypeIdx asNamed = typePool.internNamed(stringPool.intern(dotted)); + if (tit->second == asNamed) return kNoType; + return tit->second; } std::string JamCodegenContext::formatNamespaceLookupError( @@ -654,8 +778,24 @@ std::string JamCodegenContext::formatNamespaceLookupError( const JamCodegenContext::ModuleConstInfo * JamCodegenContext::getModuleConst(const std::string &name) const { - auto it = moduleConsts.find(name); - if (it != moduleConsts.end()) return &it->second; + // Resolve the bare spelling through the current module's ownership + // map to the owner-qualified registry key — a body reads its OWN + // module's const (or one it destructure-imported), never another + // module's same-named one. + auto mIt = typeModuleOf_.find(currentBodyModule()); + if (mIt != typeModuleOf_.end()) { + auto oIt = mIt->second.find(name); + if (oIt != mIt->second.end()) { + auto it = moduleConsts.find(qualifyTypeName(oIt->second, name)); + if (it != moduleConsts.end()) return &it->second; + } + } + // Entry-module bodies fall back to the bare key (entry consts + // register bare); other modules' unresolved names are NOT consts. + if (currentBodyModule().empty()) { + auto it = moduleConsts.find(name); + if (it != moduleConsts.end()) return &it->second; + } return nullptr; } @@ -676,6 +816,7 @@ inline uint64_t alignUp(uint64_t off, uint64_t align) { } // namespace uint64_t JamCodegenContext::typeSize(TypeIdx ty) const { + ty = requalifyType(ty, currentBodyModule()); const TypeKey &k = typePool.get(ty); switch (k.kind) { case TypeKind::Invalid: @@ -800,6 +941,7 @@ uint64_t JamCodegenContext::typeSize(TypeIdx ty) const { // on every target we care about. For aggregates, the alignment is the // max of the constituent alignments. uint64_t JamCodegenContext::typeAlign(TypeIdx ty) const { + ty = requalifyType(ty, currentBodyModule()); const TypeKey &k = typePool.get(ty); switch (k.kind) { case TypeKind::Invalid: @@ -959,9 +1101,59 @@ TypeIdx substituteType(TypeIdx ty, } } +// Stable spelling of a generic argument type, used to build the +// instantiated name (`Vec__i32`, `Holder__lib/m.Inner`). The spelling +// is the memoization key, so every distinct argument type MUST spell +// distinctly — a shared catch-all would silently hand `Pair(f64)` the +// struct layout already instantiated for `Pair(f32)`. Compound types +// spell recursively (`Vec(*mut u8)` -> `Vec__pm_u8`). +static std::string genericArgSpelling(const TypePool &typePool, + const StringPool &stringPool, TypeIdx t) { + const TypeKey ak = typePool.get(t); + switch (ak.kind) { + case TypeKind::Int: { + char buf[16]; + std::snprintf(buf, sizeof(buf), "%c%u", ak.b ? 'i' : 'u', ak.a); + return buf; + } + case TypeKind::Bool: + return "bool"; + case TypeKind::Float: + return ak.a == 32 ? "f32" : "f64"; + case TypeKind::Struct: + case TypeKind::Named: + case TypeKind::Enum: + case TypeKind::Union: + return stringPool.get(static_cast(ak.a)); + case TypeKind::PtrSingle: + return "p_" + genericArgSpelling(typePool, stringPool, + static_cast(ak.a)); + case TypeKind::PtrMany: + return "pm_" + genericArgSpelling(typePool, stringPool, + static_cast(ak.a)); + case TypeKind::Slice: + return "sl_" + genericArgSpelling(typePool, stringPool, + static_cast(ak.a)); + case TypeKind::Array: + return "arr" + std::to_string(ak.b) + "_" + + genericArgSpelling(typePool, stringPool, + static_cast(ak.a)); + default: + // No stable spelling — include the TypeIdx so distinct types + // never share an instantiation. + return "T" + std::to_string(t); + } +} + } // namespace TypeIdx JamCodegenContext::resolveGenericCall(TypeIdx callTy) const { + // Qualify the callee + args to their owning modules first (no-op for + // already-qualified or entry-owned names). Some callers requalify + // before calling; the ones that can't (jir_verify's hook) rely on + // this. Without it the per-TypeIdx caches below would conflate two + // modules' same-named generics. + callTy = requalifyType(callTy, currentBodyModule()); // Apply the active substitution context to the GenericCall's args // before resolving. The same `Inner(T)` TypeIdx appears in every // instantiation of an outer generic that mentions it — Wrap(i32) @@ -994,8 +1186,28 @@ TypeIdx JamCodegenContext::resolveGenericCall(TypeIdx callTy) const { if (cached != genericResolutions_.end()) return cached->second; const TypeKey &k = typePool.get(effectiveTy); - const std::string &calleeName = stringPool.get(static_cast(k.a)); - const auto &args = typePool.genericArgsAt(k.b); + const std::string calleeName = stringPool.get(static_cast(k.a)); + + // Canonicalize the args to their owner identities so the identity + // cache and the instantiated name don't depend on how the caller + // SPELLED an argument: a handle-qualified `P.Pair` resolves through + // the scoped alias table to the same `lib/p.Pair` a requalified + // bare reference produces — one instantiation, not two. + std::vector args = typePool.genericArgsAt(k.b); + for (TypeIdx &a : args) { + const TypeKey &ak = typePool.get(a); + if (ak.kind != TypeKind::Named) continue; + const std::string &an = stringPool.get(static_cast(ak.a)); + if (an.find('.') == std::string::npos) continue; + TypeIdx aliased = lookupTypeAlias(an); + if (aliased == kNoType) { + TypeIdx chained = resolveChainedType(an); + if (chained != kNoType) aliased = chained; + } + if (aliased != kNoType && aliased != a) { + a = requalifyType(aliased, currentBodyModule()); + } + } // Demand-driven: consult the Analyzer first so cycle detection // kicks in and any cross-decl dependency the user introduced is @@ -1088,19 +1300,24 @@ TypeIdx JamCodegenContext::resolveGenericCall(TypeIdx callTy) const { break; } if (value.tag == AstTag::StructExpr) { - // Use generic->Name (the bare source-level name) for the - // instantiated struct name so syntactic prefixes like - // `c.Vec` don't bake into `Vec__i32`. Pass the generic's - // modulePath so the cloned methods inherit it — anon-struct - // methods returned from `pub fn Vec(T)` aren't visible to - // module_resolver, so we propagate the originating module - // here for body-scope identifier resolution. - result = instantiateStructExpr(value, generic->Name, args, subst, - generic->modulePath); + // The instantiated struct's name starts from the generic's + // OWNER-QUALIFIED identity (`lib/a.Pair__i32`), not the + // syntactic spelling: prefixes like `c.Vec` converge on the + // owner, while two modules' same-named factories stay + // distinct. Pass the generic's modulePath so the cloned + // methods and the anon struct's member types resolve against + // the DEFINING module — anon-struct bodies returned from + // `pub fn Vec(T)` aren't visible to module_resolver, so the + // originating module is propagated here. + result = instantiateStructExpr( + value, qualifyTypeName(generic->modulePath, generic->Name), + args, subst, generic->modulePath); break; } if (value.tag == AstTag::EnumExpr) { - result = instantiateEnumExpr(value, generic->Name, args, subst); + result = instantiateEnumExpr( + value, qualifyTypeName(generic->modulePath, generic->Name), + args, subst, generic->modulePath); break; } throw std::runtime_error( @@ -1120,20 +1337,36 @@ TypeIdx JamCodegenContext::resolveGenericCall(TypeIdx callTy) const { } void JamCodegenContext::seedComptimeScope(jam::ComptimeScope &scope) const { - // Fold with every module const in scope. Consts may reference each - // other (`const B = A * 2;`), so seed the scope to a fixpoint: each - // pass folds the consts whose dependencies folded in an earlier - // pass. Consts that never fold (runtime-only inits) simply stay - // unbound — an expression referencing one comes back None. + // Fold the consts VISIBLE TO THE CURRENT MODULE into scope, bound + // by their bare source spelling: a body references `N`, and `N` + // must be its own module's (or a destructure-imported) const — + // never another module's same-named one, and never a private const + // of an unrelated module. Consts may reference each other + // (`const B = A * 2;`), so seed to a fixpoint: each pass folds the + // consts whose dependencies folded in an earlier pass. Consts that + // never fold (runtime-only inits) simply stay unbound — an + // expression referencing one comes back None. jam::ComptimeEvaluator ev(nodeStore, stringPool, typePool); + const std::string &cur = currentBodyModule(); + auto ownIt = typeModuleOf_.find(cur); + auto visible = [&](const ModuleConstInfo &info) -> bool { + if (ownIt != typeModuleOf_.end()) { + auto oIt = ownIt->second.find(info.bareName); + if (oIt != ownIt->second.end()) return oIt->second == info.owner; + } + // Entry bodies additionally see entry consts that predate the + // ownership map (bare-registered, owner ""). + return cur.empty() && info.owner.empty(); + }; bool progress = true; while (progress) { progress = false; for (const auto &kv : moduleConsts) { - if (scope.lookup(kv.first) != nullptr) continue; + if (!visible(kv.second)) continue; + if (scope.lookup(kv.second.bareName) != nullptr) continue; jam::ComptimeValue v = ev.eval(kv.second.initExpr, scope); if (!v.isNone()) { - scope.bind(kv.first, v); + scope.bind(kv.second.bareName, v); progress = true; } } @@ -1277,33 +1510,14 @@ TypeIdx JamCodegenContext::instantiateStructExpr( } const StructDeclAST *anon = (*anonStructs_)[anonIdx].get(); - // Build the instantiated struct's name from the callee + arg names. - // `Maybe(File)` -> `Maybe__File`. Pointer/array types lower through - // substituteType; we only need a stable spelling for the canonical - // non-compound cases here. v1's stdlib won't pass non-named types - // as generic args, so this is enough to get the demo running. + // Build the instantiated struct's name from the callee's qualified + // identity + arg spellings: `Maybe(File)` -> `Maybe__std/fs.File`. + // See genericArgSpelling for why every distinct arg type must spell + // distinctly. std::string instName = calleeName; for (TypeIdx a : args) { instName += "__"; - const TypeKey &ak = typePool.get(a); - switch (ak.kind) { - case TypeKind::Int: { - char buf[16]; - std::snprintf(buf, sizeof(buf), "%c%u", ak.b ? 'i' : 'u', ak.a); - instName += buf; - break; - } - case TypeKind::Bool: - instName += "bool"; - break; - case TypeKind::Struct: - case TypeKind::Named: - instName += stringPool.get(static_cast(ak.a)); - break; - default: - instName += "T"; // catch-all; v2 spec needed - break; - } + instName += genericArgSpelling(typePool, stringPool, a); } // Memoize on the instantiated name. If we've already produced this @@ -1328,11 +1542,18 @@ TypeIdx JamCodegenContext::instantiateStructExpr( bodySubst["Self"] = instNamed; // Substitute each field's type, then declare + fill the LLVM struct. + // After substitution, qualify against the DEFINING module: a field + // like `inner: Inner` names the generic's sibling type, not whatever + // `Inner` happens to mean at the trigger site. Anon-struct bodies + // never pass through bindDeclTypes, so this is their decl-time + // qualification. std::vector> instFields; instFields.reserve(anon->Fields.size()); for (const auto &f : anon->Fields) { - instFields.emplace_back( - f.first, substituteType(f.second, bodySubst, typePool, stringPool)); + TypeIdx subbed = + substituteType(f.second, bodySubst, typePool, stringPool); + instFields.emplace_back(f.first, + requalifyType(subbed, definingModulePath_)); } JamTypeRef llvmStruct = @@ -1370,19 +1591,25 @@ TypeIdx JamCodegenContext::instantiateStructExpr( JamCodegenContext &mutCtx = const_cast(*this); // Pass 1: clone + register + jirDeclarePrototype for every method. + // Signature types qualify against the DEFINING module after + // substitution (same as the field types above): a method like + // `fn makeInner(self: Self) Inner` names the generic's sibling + // type regardless of what the trigger site calls `Inner`. for (const auto &origMethod : anon->Methods) { std::vector instArgs; instArgs.reserve(origMethod->Args.size()); for (const auto &p : origMethod->Args) { Param sp = p; - sp.Type = - substituteType(p.Type, bodySubst, typePool, stringPool); + sp.Type = requalifyType( + substituteType(p.Type, bodySubst, typePool, stringPool), + definingModulePath_); instArgs.push_back(std::move(sp)); } TypeIdx instReturn = origMethod->ReturnType; if (instReturn != kNoType) { - instReturn = - substituteType(instReturn, bodySubst, typePool, stringPool); + instReturn = requalifyType( + substituteType(instReturn, bodySubst, typePool, stringPool), + definingModulePath_); } std::string instMethodName = instName + "." + origMethod->Name; @@ -1547,7 +1774,8 @@ TypeIdx JamCodegenContext::instantiateStructExpr( TypeIdx JamCodegenContext::instantiateEnumExpr( const AstNode &exprNode, const std::string &calleeName, const std::vector &args, - const std::unordered_map &subst) const { + const std::unordered_map &subst, + const std::string &definingModulePath) const { if (!anonEnums_) { throw std::runtime_error( "internal: anonymous enum table not registered on " @@ -1561,30 +1789,12 @@ TypeIdx JamCodegenContext::instantiateEnumExpr( } const EnumDeclAST *anon = (*anonEnums_)[anonIdx].get(); - // Build instantiated name `Option__i32` etc — same shape as struct. + // Build instantiated name `Option__i32` etc — same shape (and same + // shared speller) as the struct path. std::string instName = calleeName; for (TypeIdx a : args) { instName += "__"; - const TypeKey &ak = typePool.get(a); - switch (ak.kind) { - case TypeKind::Int: { - char buf[16]; - std::snprintf(buf, sizeof(buf), "%c%u", ak.b ? 'i' : 'u', ak.a); - instName += buf; - break; - } - case TypeKind::Bool: - instName += "bool"; - break; - case TypeKind::Struct: - case TypeKind::Enum: - case TypeKind::Named: - instName += stringPool.get(static_cast(ak.a)); - break; - default: - instName += "T"; - break; - } + instName += genericArgSpelling(typePool, stringPool, a); } // Memoize. Return as a Named TypeIdx so the rest of codegen resolves @@ -1609,8 +1819,13 @@ TypeIdx JamCodegenContext::instantiateEnumExpr( vi.name = v.Name; vi.discriminant = v.Discriminant; for (TypeIdx ty : v.PayloadTypes) { - vi.payloadTypes.push_back( - substituteType(ty, bodySubst, typePool, stringPool)); + // Qualify against the DEFINING module after substitution — + // a payload naming the generic's sibling type must not + // capture the trigger site's same-named type. Mirrors the + // struct path's field qualification. + vi.payloadTypes.push_back(requalifyType( + substituteType(ty, bodySubst, typePool, stringPool), + definingModulePath)); } if (!vi.payloadTypes.empty()) hasPayload = true; variants.push_back(std::move(vi)); diff --git a/src/codegen.h b/src/codegen.h index 87b4bb5..67ec3bc 100644 --- a/src/codegen.h +++ b/src/codegen.h @@ -22,6 +22,17 @@ #include #include +// Module-qualified identity for a type/decl: `modulePath.name`, or just +// `name` for the entry module (empty modulePath). This is the single key +// under which a type registers and resolves, so two modules that each +// define `Thing` get distinct identities (`a.Thing` vs `lib/b.Thing`). +// Mirrors how `mangledFunctionName` qualifies functions by modulePath. +inline std::string qualifyTypeName(const std::string &modulePath, + const std::string &name) { + if (modulePath.empty()) return name; + return modulePath + "." + name; +} + // Forward declarations for AST types referenced by pointer/reference // here. Full definitions live in ast.h, included by ast.cpp / codegen.cpp. class FunctionAST; @@ -89,6 +100,26 @@ class JamCodegenContext { int getFieldIndex(const std::string &structName, const std::string &fieldName) const; + // Record that, within module `ctxModule`, the bare type name + // `typeName` denotes a type owned by `ownerModule` — either because + // `ctxModule` declares it (ownerModule == ctxModule) or imports it + // (a destructuring import names another module's `pub` type). Built + // once up front in main.cpp; consulted by `requalifyType`. + void registerTypeOwner(const std::string &ctxModule, + const std::string &typeName, + const std::string &ownerModule); + + // Rewrite a type so every bare user-type leaf is replaced by its + // module-qualified identity, as seen from `ctxModule`. A bare + // `Named("Thing")` referenced inside module `m` becomes + // `Named("m.Thing")` (own type) or `Named(".Thing")` (imported); + // pointer/slice/array/generic-call wrappers are rewritten recursively. + // Names already containing a `.` (handle/chained) and names with no + // known owner (std, generic params, `Self`, builtins) are left as-is. + // This is what gives same-named types in different modules distinct + // TypeIdxs, which the per-TypeIdx LLVM-type cache relies on. + TypeIdx requalifyType(TypeIdx ty, const std::string &ctxModule) const; + // Union registry. Untagged unions: every field shares the same // address. UnionInfo carries the union's LLVM storage type plus the // list of (fieldName, fieldType) pairs for member-access lookup. @@ -160,16 +191,28 @@ class JamCodegenContext { // fold at compile time, validated eagerly after registration (see // validateCompConsts) instead of lazily at first comp-position use. // `file` records the declaring module's path so the eager - // validation can anchor its diagnostic. + // validation can anchor its diagnostic. `bareName`/`owner` carry + // the source spelling and owning module so the comptime-scope + // seeding can decide per-module visibility and bind the name the + // body actually spells. struct ModuleConstInfo { NodeIdx initExpr; TypeIdx declaredType; bool isComp = false; std::string file; + std::string bareName; + std::string owner; }; - void registerModuleConst(const std::string &name, NodeIdx init, + // `name` is the owner-qualified registry key (`lib/a.LIMIT`; bare + // for the entry module). + void registerModuleConst(const std::string &name, + const std::string &bareName, + const std::string &owner, NodeIdx init, TypeIdx declared, bool isComp = false, std::string file = std::string()); + // Resolve a body's bare const spelling against the CURRENT module: + // the ownership map gives the owner, the qualified key indexes the + // registry. A module never reads another module's same-named const. const ModuleConstInfo *getModuleConst(const std::string &name) const; // Eager `comp const` validation: every registered module const @@ -318,6 +361,13 @@ class JamCodegenContext { mutable std::map structs; std::map unions; mutable std::map enums; + // Per-module type-name origin: typeModuleOf_[ctxModule][typeName] = + // owning module path. Lets `requalifyType` map a bare `Thing` in + // module `m` to the module that actually declares it (m itself, or a + // destructuring-import source). Populated up front in main.cpp. + std::unordered_map> + typeModuleOf_; std::map moduleConsts; // (FunctionAST*, IfNode NodeIdx) -> comp-if condition verdict. See // recordCompIfVerdict / lookupCompIfVerdict above. @@ -613,10 +663,29 @@ class JamCodegenContext { 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; + // `const Name = T` aliases register under their owner-qualified + // identity. A dotted name IS such an identity (or a handle + // spelling, already missed above) — look it up directly. A bare + // name resolves through the current module's ownership map, so + // a body sees its own module's alias (or a destructure-imported + // one), never another module's same-named alias. + if (name.find('.') != std::string::npos) { + auto it = typeAliases_.find(name); + return it != typeAliases_.end() ? it->second : kNoType; + } + auto oIt = typeModuleOf_.find(currentBodyModule()); + if (oIt != typeModuleOf_.end()) { + auto tIt = oIt->second.find(name); + if (tIt != oIt->second.end()) { + auto it = typeAliases_.find(qualifyTypeName(tIt->second, name)); + if (it != typeAliases_.end()) return it->second; + } + } + // Entry bodies: entry aliases register bare. + if (currentBodyModule().empty()) { + auto it = typeAliases_.find(name); + if (it != typeAliases_.end()) return it->second; + } return kNoType; } @@ -683,10 +752,11 @@ class JamCodegenContext { // returns an Enum TypeIdx pointing at it. Memoizes by instantiated // name. Used to materialize `Option(i32)`, `Result(File, Errno)`, // and other sum-type generics on demand. - TypeIdx instantiateEnumExpr( - const AstNode &exprNode, const std::string &calleeName, - const std::vector &args, - const std::unordered_map &subst) const; + TypeIdx + instantiateEnumExpr(const AstNode &exprNode, const std::string &calleeName, + const std::vector &args, + const std::unordered_map &subst, + const std::string &definingModulePath) const; }; // Resolves comptime `Call` nodes (cfn -> cfn) for a ComptimeEvaluator. diff --git a/src/init_analysis.cpp b/src/init_analysis.cpp index 4a1d149..4cb4bfa 100644 --- a/src/init_analysis.cpp +++ b/src/init_analysis.cpp @@ -177,7 +177,7 @@ class Analyzer { if (an.tag != AstTag::Variable) continue; const std::string &src = strings_.get(static_cast(an.lhs)); - if (lookupDropFor(src) != nullptr) { + if (bindingIsDropBearing(src)) { applyMoveToBinding(src, argIdx, state); } } @@ -231,6 +231,21 @@ class Analyzer { // no drop is registered, when the binding's type is unknown, or when // the drop or type registry is unavailable. const FunctionAST *lookupDropFor(const std::string &bindingName) const; + + // Move-gate oracle: is `bindingName`'s type drop-bearing? Two + // answers combined: the drops registry (struct-name keyed — misses + // imported types, whose registry keys are module-qualified) and the + // codegen typeNeedsDrop hook (which requalifies, and also covers + // enums with dropping payloads, arrays of drop-bearing elements, + // structs whose FIELDS drop, and generic instantiations). Every + // "bare drop-bearing use is a MOVE" gate must consult this, not the + // raw registry, or moves of imported types go untracked. + bool bindingIsDropBearing(const std::string &bindingName) const { + if (lookupDropFor(bindingName) != nullptr) return true; + auto vt = varTypes_.find(bindingName); + if (vt == varTypes_.end()) return false; + return typeOwnsDrops(vt->second); + } }; // Entry point. @@ -407,7 +422,7 @@ Result Analyzer::analyze(NodeIdx idx, NameMap state) { if (elemNode.tag == AstTag::Variable) { const std::string &src = strings_.get(static_cast(elemNode.lhs)); - if (lookupDropFor(src) != nullptr) { + if (bindingIsDropBearing(src)) { applyMoveToBinding(src, elemIdx, r.state); } } @@ -507,7 +522,7 @@ Result Analyzer::analyzeVarDecl(NodeIdx idx, NameMap state) { if (initNode.tag == AstTag::Variable) { const std::string &src = strings_.get(static_cast(initNode.lhs)); - if (lookupDropFor(src) != nullptr) { + if (bindingIsDropBearing(src)) { applyMoveToBinding(src, initIdx, r.state); } } @@ -543,7 +558,7 @@ Result Analyzer::analyzeAssign(NodeIdx idx, NameMap state) { if (rhsNode.tag == AstTag::Variable) { const std::string &src = strings_.get(static_cast(rhsNode.lhs)); - if (lookupDropFor(src) != nullptr) { + if (bindingIsDropBearing(src)) { applyMoveToBinding(src, n.rhs, r.state); } } @@ -860,7 +875,17 @@ Result Analyzer::analyzeCall(NodeIdx idx, NameMap state) { if (isEnumVariantCtor(recv, method)) { isVariantCtor = true; } auto vt = varTypes_.find(recv); if (vt != varTypes_.end()) { - const TypeKey &k = types_->get(vt->second); + // Requalify the receiver's type first: imported + // structs' methods register under the struct's + // module-qualified identity, while a local's + // annotation carries the bare spelling. The hook + // is null only in standalone unit tests, where + // bare keys are correct. + TypeIdx recvTy = vt->second; + if (hooks_ != nullptr && hooks_->requalifyType != nullptr) { + recvTy = hooks_->requalifyType(hooks_->ctx, recvTy); + } + const TypeKey &k = types_->get(recvTy); if (k.kind == TypeKind::Struct || k.kind == TypeKind::Named || k.kind == TypeKind::GenericCall) { @@ -984,16 +1009,7 @@ 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. - // 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) { + if (bindingIsDropBearing(name)) { auto dd = declDepth_.find(name); if (dd == declDepth_.end()) { if (args_ != nullptr) { @@ -1177,7 +1193,7 @@ Result Analyzer::analyzeStructLit(NodeIdx idx, NameMap state) { if (fieldExpr.tag == AstTag::Variable) { const std::string &fname = strings_.get(static_cast(fieldExpr.lhs)); - if (lookupDropFor(fname) != nullptr) { + if (bindingIsDropBearing(fname)) { applyMoveToBinding(fname, exprIdx, r.state); } } @@ -1261,14 +1277,7 @@ void Analyzer::checkDropBearingLocalsInit(const NameMap &state, } if (isBorrowedParam) continue; - auto vt = varTypes_.find(name); - if (vt == varTypes_.end()) continue; - const TypeKey &k = types_->get(vt->second); - if (k.kind != TypeKind::Struct && k.kind != TypeKind::Named) continue; - StringIdx structNameIdx = static_cast(k.a); - if (structNameIdx == kNoString) continue; - const std::string &structName = strings_.get(structNameIdx); - if (drops_->find(structName) == drops_->end()) continue; + if (!bindingIsDropBearing(name)) continue; InitState s = sv.second; if (s == InitState::Init) continue; @@ -1281,8 +1290,21 @@ void Analyzer::checkDropBearingLocalsInit(const NameMap &state, continue; } + // Recover the source-spelled type name for the diagnostic. + std::string typeName = "?"; + if (auto vt = varTypes_.find(name); + vt != varTypes_.end() && types_ != nullptr) { + const TypeKey &k = types_->get(vt->second); + StringIdx ni = static_cast(k.a); + if ((k.kind == TypeKind::Struct || k.kind == TypeKind::Named || + k.kind == TypeKind::Enum || k.kind == TypeKind::Union || + k.kind == TypeKind::GenericCall) && + ni != kNoString) { + typeName = strings_.get(ni); + } + } std::string msg = - "drop-bearing binding `" + name + "` of type `" + structName + "` "; + "drop-bearing binding `" + name + "` of type `" + typeName + "` "; if (s == InitState::Uninit) { msg += "must be initialized before this exit; drop runs on it " "and would otherwise read uninit memory"; diff --git a/src/init_analysis.h b/src/init_analysis.h index 96fcb39..21d9d29 100644 --- a/src/init_analysis.h +++ b/src/init_analysis.h @@ -81,6 +81,14 @@ using EnumVariantMap = struct AnalysisHooks { void *ctx = nullptr; bool (*typeNeedsDrop)(void *ctx, TypeIdx ty) = nullptr; + // Rewrite a bare user-type reference to its module-qualified + // identity as seen from the function under analysis. The analyzer + // builds string keys (`Type.method`) from local-var annotation + // TypeIdxs, which carry bare source spellings; the registries key + // by qualified identity, so the analyzer must agree with codegen on + // the name. Null in standalone unit tests — callers fall back to + // the bare spelling. + TypeIdx (*requalifyType)(void *ctx, TypeIdx ty) = nullptr; // `comp if` verdict recorded by astgen for (fnAst, IfNode). The // analyzer must observe the same dead-arm elision astgen performed: // only the taken arm exists, and its statements run at the SAME diff --git a/src/jir.h b/src/jir.h index 1ae02d7..89cd150 100644 --- a/src/jir.h +++ b/src/jir.h @@ -309,6 +309,11 @@ class JirFunction { // in; carrying intent keeps the door open. std::vector paramModes; TypeIdx returnType = kNoType; + // Owning module's identity (FunctionAST::modulePath). The JIR- + // consuming passes (verify, init-analysis, define-body) push this as + // the body module so bare body-level type references resolve to their + // owning module — the same role currentBodyModule() plays in astgen. + std::string modulePath; bool isExtern = false; bool isExport = false; bool isPub = false; diff --git a/src/main.cpp b/src/main.cpp index 90a27d7..871dfb4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -133,7 +133,13 @@ static int compileAndRun(const std::string &filename, auto resolveImportChain = [&](const std::string &basePath, const std::vector &chain, auto &self) -> std::pair { - std::string curPath = basePath; + // Canonicalize the spelling to the module's identity key so the + // path stored back on the import (and every registry keyed by + // it — symbol table, handle flats, type qualification) matches + // the key the resolver cached the module under. Entry-module + // spellings (`import("fs")`, `import("./b")`) converge with + // nested ones here. + std::string curPath = resolver.canonicalKey(basePath); ModuleAST *curMod = resolver.getOrLoadModule(curPath); if (!curMod) return {curPath, nullptr}; for (const auto &seg : chain) { @@ -201,6 +207,88 @@ static int compileAndRun(const std::string &filename, codegenCtx.setAnonStructs(&sharedAnonStructs); codegenCtx.setAnonEnums(&sharedAnonEnums); + // Build the per-module type-origin map before any qualified + // registration: for each module, every type name it can refer to by + // a bare identifier maps to the module that owns it — itself for its + // own declarations, or the source module for a destructuring import + // (`const { Thing } = import("lib/b")` makes `Thing` mean + // `lib/b.Thing` inside the importer). `requalifyType` consults this + // to rewrite bare type references to their qualified identities. + auto recordTypeOwners = [&](ModuleAST *m, const std::string &P) { + for (auto &s : m->Structs) codegenCtx.registerTypeOwner(P, s->Name, P); + for (auto &e : m->Enums) codegenCtx.registerTypeOwner(P, e->Name, P); + for (auto &u : m->Unions) codegenCtx.registerTypeOwner(P, u->Name, P); + // Generic type factories (`fn Vec(T: type) type`) participate in + // type identity exactly like concrete types: a bare `Vec(u8)` + // reference qualifies to its owner so two modules' same-named + // generics never share an instantiation. + for (auto &fn : m->Functions) { + if (fn->isGeneric()) codegenCtx.registerTypeOwner(P, fn->Name, P); + } + // Module consts (value consts AND `const X = ` aliases) + // own their bare name too: getModuleConst / lookupTypeAlias + // resolve a body's bare reference through this map to the + // owner-qualified registry key, so two modules' same-named + // consts never read each other's values. + for (auto &c : m->Consts) codegenCtx.registerTypeOwner(P, c->Name, P); + for (auto &di : m->DestructuringImports) { + if (di->Path == "test") continue; + ModuleAST *src = resolver.getOrLoadModule(di->Path); + if (!src) continue; + for (const auto &name : di->Names) { + bool isType = false; + for (auto &s : src->Structs) + if (s->Name == name) isType = true; + for (auto &e : src->Enums) + if (e->Name == name) isType = true; + for (auto &u : src->Unions) + if (u->Name == name) isType = true; + for (auto &fn : src->Functions) + if (fn->isGeneric() && fn->isPub && fn->Name == name) + isType = true; + for (auto &c : src->Consts) + if (c->isPub && c->Name == name) isType = true; + if (isType) codegenCtx.registerTypeOwner(P, name, di->Path); + } + } + }; + for (const auto &[path, importedModule] : resolver.getLoadedModules()) + recordTypeOwners(importedModule.get(), path); + recordTypeOwners(module.get(), /*P=*/""); + + // Bind every declaration-level type reference (function signatures, + // struct/union fields, enum payloads) to its module-qualified + // identity in place. After this, registration, the analyzer, + // prototype emission, and call-site return-type resolution all read + // already-qualified types — no body-module context needed. Body-level + // references (struct-literal types, local annotations) live in the + // node store and are qualified later at astgen time via + // currentBodyModule. + auto bindDeclTypes = [&](ModuleAST *m) { + auto rqFn = [&](FunctionAST *fn) { + fn->ReturnType = + codegenCtx.requalifyType(fn->ReturnType, fn->modulePath); + for (auto &p : fn->Args) + p.Type = codegenCtx.requalifyType(p.Type, fn->modulePath); + }; + for (auto &fn : m->Functions) rqFn(fn.get()); + for (auto &s : m->Structs) { + for (auto &f : s->Fields) + f.second = codegenCtx.requalifyType(f.second, s->modulePath); + for (auto &mm : s->Methods) rqFn(mm.get()); + } + for (auto &e : m->Enums) + for (auto &v : e->Variants) + for (auto &pt : v.PayloadTypes) + pt = codegenCtx.requalifyType(pt, e->modulePath); + for (auto &u : m->Unions) + for (auto &f : u->Fields) + f.second = codegenCtx.requalifyType(f.second, u->modulePath); + }; + for (const auto &[path, importedModule] : resolver.getLoadedModules()) + bindDeclTypes(importedModule.get()); + bindDeclTypes(module.get()); + // Populate the demand-driven DeclTable: one Decl per top-level // binding across the main module + every imported module. The // analyzer consults this table for cycle detection when codegen @@ -219,40 +307,54 @@ static int compileAndRun(const std::string &filename, }; for (auto &fn : m->Functions) { if (publicOnly && !fn->isPub) continue; + // Generic factories key by their qualified identity — the + // requalified GenericCall callee that resolveGenericCall + // looks up. Bare-keying them lets one module's `Pair` + // shadow another's in the bare-first-wins byName table. + // Plain fns keep their bare source name. + std::string declName = + fn->isGeneric() ? qualifyTypeName(fn->modulePath, fn->Name) + : fn->Name; jam::DeclIndex idx = codegenCtx.declTable().create( - jam::DeclKind::Function, fn->Name); + jam::DeclKind::Function, std::move(declName)); auto &d = codegenCtx.declTable().get(idx); d.fnAst = fn.get(); setSrc(d, fn->Name); } + // Types register under their module-qualified identity so two + // modules that each declare `Thing` get distinct DeclTable + // entries (and the analyzer, which keys layout off the decl + // name, resolves the right one). The qualified name matches the + // key used by the struct/enum/union registries below and by the + // requalified field/body TypeIdxs that reference them. for (auto &s : m->Structs) { if (publicOnly && !s->isPub) continue; - jam::DeclIndex idx = - codegenCtx.declTable().create(jam::DeclKind::Struct, s->Name); + jam::DeclIndex idx = codegenCtx.declTable().create( + jam::DeclKind::Struct, qualifyTypeName(s->modulePath, s->Name)); auto &d = codegenCtx.declTable().get(idx); d.structAst = s.get(); setSrc(d, s->Name); } for (auto &e : m->Enums) { if (publicOnly && !e->isPub) continue; - jam::DeclIndex idx = - codegenCtx.declTable().create(jam::DeclKind::Enum, e->Name); + jam::DeclIndex idx = codegenCtx.declTable().create( + jam::DeclKind::Enum, qualifyTypeName(e->modulePath, e->Name)); auto &d = codegenCtx.declTable().get(idx); d.enumAst = e.get(); setSrc(d, e->Name); } for (auto &u : m->Unions) { if (publicOnly && !u->isPub) continue; - jam::DeclIndex idx = - codegenCtx.declTable().create(jam::DeclKind::Union, u->Name); + jam::DeclIndex idx = codegenCtx.declTable().create( + jam::DeclKind::Union, qualifyTypeName(u->modulePath, u->Name)); auto &d = codegenCtx.declTable().get(idx); d.unionAst = u.get(); setSrc(d, u->Name); } for (auto &c : m->Consts) { if (publicOnly && !c->isPub) continue; - jam::DeclIndex idx = - codegenCtx.declTable().create(jam::DeclKind::Const, c->Name); + jam::DeclIndex idx = codegenCtx.declTable().create( + jam::DeclKind::Const, qualifyTypeName(c->modulePath, c->Name)); auto &d = codegenCtx.declTable().get(idx); d.constAst = c.get(); setSrc(d, c->Name); @@ -267,20 +369,39 @@ static int compileAndRun(const std::string &filename, // items get registered, so non-pub items can't leak into the // importing module via bare-name lookup. Main-module decls always // register since there's no outer consumer. + // Each type registers under its module-qualified identity + // (`qualifyTypeName`), and its field / payload TypeIdxs are + // requalified to the same scheme so a field typed `Inner` resolves + // to *this* module's `Inner`. requalifyType consults the type-origin + // map built above, so it doesn't depend on registration order. + auto requalFields = + [&](const std::vector> &fields, + const std::string &mod) { + std::vector> out; + out.reserve(fields.size()); + for (const auto &f : fields) + out.emplace_back(f.first, + codegenCtx.requalifyType(f.second, mod)); + return out; + }; auto declareStructs = [&](ModuleAST *m, bool publicOnly) { for (auto &s : m->Structs) { if (publicOnly && !s->isPub) continue; - JamTypeRef structType = JamLLVMStructCreateNamed( - codegenCtx.getContext(), s->Name.c_str()); - codegenCtx.registerStruct(s->Name, structType, s->Fields); + std::string q = qualifyTypeName(s->modulePath, s->Name); + JamTypeRef structType = + JamLLVMStructCreateNamed(codegenCtx.getContext(), q.c_str()); + codegenCtx.registerStruct(q, structType, + requalFields(s->Fields, s->modulePath)); } }; auto declareUnions = [&](ModuleAST *m, bool publicOnly) { for (auto &u : m->Unions) { if (publicOnly && !u->isPub) continue; - JamTypeRef unionType = JamLLVMStructCreateNamed( - codegenCtx.getContext(), u->Name.c_str()); - codegenCtx.registerUnion(u->Name, unionType, u->Fields); + std::string q = qualifyTypeName(u->modulePath, u->Name); + JamTypeRef unionType = + JamLLVMStructCreateNamed(codegenCtx.getContext(), q.c_str()); + codegenCtx.registerUnion(q, unionType, + requalFields(u->Fields, u->modulePath)); } }; auto declareEnums = [&](ModuleAST *m, bool publicOnly) { @@ -291,11 +412,15 @@ static int compileAndRun(const std::string &filename, for (auto &v : e->Variants) { JamCodegenContext::EnumVariantInfo info; info.name = v.Name; - info.payloadTypes = v.PayloadTypes; + info.payloadTypes.reserve(v.PayloadTypes.size()); + for (TypeIdx pt : v.PayloadTypes) + info.payloadTypes.push_back( + codegenCtx.requalifyType(pt, e->modulePath)); info.discriminant = v.Discriminant; variants.push_back(std::move(info)); } - codegenCtx.registerEnum(e->Name, std::move(variants)); + codegenCtx.registerEnum(qualifyTypeName(e->modulePath, e->Name), + std::move(variants)); } }; // Enums that need a named struct type (i.e. those with payload @@ -303,11 +428,12 @@ static int compileAndRun(const std::string &filename, // that fillEnumBodies can set the body in a second pass. auto declareEnumLLVMTypes = [&](ModuleAST *m) { for (auto &e : m->Enums) { - const auto *info = codegenCtx.getEnum(e->Name); + std::string q = qualifyTypeName(e->modulePath, e->Name); + const auto *info = codegenCtx.getEnum(q); if (!info || !info->hasPayloadVariant) continue; - JamTypeRef ty = JamLLVMStructCreateNamed(codegenCtx.getContext(), - e->Name.c_str()); - codegenCtx.setEnumLLVMType(e->Name, ty, 0, 1, true); + JamTypeRef ty = + JamLLVMStructCreateNamed(codegenCtx.getContext(), q.c_str()); + codegenCtx.setEnumLLVMType(q, ty, 0, 1, true); } }; for (const auto &[path, importedModule] : resolver.getLoadedModules()) { @@ -347,6 +473,16 @@ static int compileAndRun(const std::string &filename, // regardless of pub status. Only pub ones leak into the // global flat map. ns.functions[func->Name] = func.get(); + // Generics (pub AND private — a private generic is reachable + // from its own module's bodies) also register under their + // qualified identity: the requalified GenericCall callee + // (`lib/a.Pair`) that resolveGenericCall looks up. Qualified + // keys contain `/` or `.`, so they can't collide with bare + // source names. + if (func->isGeneric()) { + codegenCtx.registerFunctionAST( + qualifyTypeName(path, func->Name), func.get()); + } if (func->isPub) { codegenCtx.registerFunctionAST(func->Name, func.get()); // Eagerly declare LLVM prototypes for pub-extern fns @@ -367,22 +503,29 @@ static int compileAndRun(const std::string &filename, } } } + // The namespace maps a bare member name (`Counter`) to the type's + // qualified identity (`a.Counter`), so `a.Counter` from an + // importer resolves to *this* module's struct, not a same-named + // one elsewhere. for (auto &s : importedModule->Structs) { if (s->isPub) { ns.types[s->Name] = codegenCtx.getTypePool().internNamed( - codegenCtx.getStringPool().intern(s->Name)); + codegenCtx.getStringPool().intern( + qualifyTypeName(path, s->Name))); } } for (auto &e : importedModule->Enums) { if (e->isPub) { ns.types[e->Name] = codegenCtx.getTypePool().internNamed( - codegenCtx.getStringPool().intern(e->Name)); + codegenCtx.getStringPool().intern( + qualifyTypeName(path, e->Name))); } } for (auto &u : importedModule->Unions) { if (u->isPub) { ns.types[u->Name] = codegenCtx.getTypePool().internNamed( - codegenCtx.getStringPool().intern(u->Name)); + codegenCtx.getStringPool().intern( + qualifyTypeName(path, u->Name))); } } for (auto &reexport : importedModule->Imports) { @@ -435,11 +578,22 @@ static int compileAndRun(const std::string &filename, auto regPriv = [&](const std::string &name) { codegenCtx.registerScopedPrivateName(owner, handle, name); }; + // `handle.Type` (e.g. `a.Counter`) aliases to the type's + // qualified identity in its owning module, so two imported + // modules' same-named types resolve to distinct structs. + // When the handle spelling IS the qualified identity (a handle + // named like the module: `const m = import("m")`), registering + // the alias would map the name to itself — and every alias + // consumer tail-recurses on the target, so a self-alias spins + // forever. The registries already hold the identity directly; + // skip the no-op. auto aliasNamed = [&](const std::string &bare) { + std::string qualified = qualifyTypeName(modulePath, bare); + std::string key = handle + "." + bare; + if (key == qualified) return; TypeIdx target = codegenCtx.getTypePool().internNamed( - codegenCtx.getStringPool().intern(bare)); - codegenCtx.registerScopedTypeAlias(owner, handle + "." + bare, - target); + codegenCtx.getStringPool().intern(qualified)); + codegenCtx.registerScopedTypeAlias(owner, key, target); }; for (auto &func : importedModule->Functions) { if (func->isPub) regFn(handle + "." + func->Name, func.get()); @@ -618,10 +772,20 @@ static int compileAndRun(const std::string &filename, return kNoType; }; + // Consts and aliases register under their owner-qualified identity + // (`lib/a.LIMIT`), matching the ownership map recordTypeOwners + // built — getModuleConst / lookupTypeAlias resolve a body's bare + // spelling through that map, so two modules' same-named consts stay + // separate. Alias TARGETS requalify against the alias's own module + // so `pub const W = Holder(Inner)` captures the owner's `Inner`, + // not the consumer's. auto registerConsts = [&](ModuleAST *m, const std::string &file) { for (auto &c : m->Consts) { + std::string qname = qualifyTypeName(c->modulePath, c->Name); if (c->AliasedType != kNoType) { - codegenCtx.registerTypeAlias(c->Name, c->AliasedType); + codegenCtx.registerTypeAlias( + qname, + codegenCtx.requalifyType(c->AliasedType, c->modulePath)); continue; } if (c->InitExpr != kNoNode) { @@ -634,14 +798,16 @@ static int compileAndRun(const std::string &filename, // builtin (i32) or a Named lookup that doesn't // resolve fall through to value-const behavior. if (k.kind == TypeKind::GenericCall) { - c->AliasedType = maybeAlias; - codegenCtx.registerTypeAlias(c->Name, c->AliasedType); + c->AliasedType = + codegenCtx.requalifyType(maybeAlias, c->modulePath); + codegenCtx.registerTypeAlias(qname, c->AliasedType); continue; } } } - codegenCtx.registerModuleConst(c->Name, c->InitExpr, - c->DeclaredType, c->isComp, file); + codegenCtx.registerModuleConst(qname, c->Name, c->modulePath, + c->InitExpr, c->DeclaredType, + c->isComp, file); } }; for (const auto &[path, importedModule] : resolver.getLoadedModules()) { @@ -699,6 +865,17 @@ static int compileAndRun(const std::string &filename, // modules resolve last-wins. fnRegistry[fn->Name] = fn.get(); } + // Imported structs' methods, under the struct's QUALIFIED + // identity — the analyzer's method-mode lookup requalifies the + // receiver's type (via the requalifyType hook) and builds + // `.method`, so a move-mode arg on an imported + // type's method is tracked like a same-module one. + for (auto &s : kv.second->Structs) { + std::string qself = qualifyTypeName(s->modulePath, s->Name); + for (auto &m : s->Methods) { + fnRegistry[qself + "." + m->Name] = m.get(); + } + } } // Methods of generic struct-returning functions register under // "GenericName.method" — modes don't depend on T. @@ -715,8 +892,13 @@ static int compileAndRun(const std::string &filename, uint32_t anonIdx = value.lhs; if (anonIdx >= sharedAnonStructs.size()) break; const StructDeclAST *anon = sharedAnonStructs[anonIdx].get(); + // Key by the factory's QUALIFIED identity — the + // analyzer's method-mode lookup requalifies the + // receiver's GenericCall callee before building + // `.method`, so the keys must agree. + std::string qfn = qualifyTypeName(fn->modulePath, fn->Name); for (const auto &mth : anon->Methods) { - fnRegistry[fn->Name + "." + mth->Name] = mth.get(); + fnRegistry[qfn + "." + mth->Name] = mth.get(); } break; } @@ -765,6 +947,13 @@ static int compileAndRun(const std::string &filename, analysisHooks.typeNeedsDrop = +[](void *c, TypeIdx t) -> bool { return typeNeedsDrop(*static_cast(c), t); }; + // Qualified-identity oracle: requalify against the module of the + // function under analysis (runAnalysisIn pushes it as the body + // module before calling analyze()). + analysisHooks.requalifyType = +[](void *c, TypeIdx t) -> TypeIdx { + auto *cc = static_cast(c); + return cc->requalifyType(t, cc->currentBodyModule()); + }; // `comp if` verdict oracle: astgen records which arm it lowered; // the analyzer walks only that arm (see Analyzer::analyzeIf). analysisHooks.compIfVerdict = @@ -845,6 +1034,11 @@ static int compileAndRun(const std::string &filename, // in module B is invisible to module A's call sites. auto registerStructMethods = [&](ModuleAST *m, bool publicOnly) -> int { for (auto &s : m->Structs) { + // Methods register and dispatch under the struct's qualified + // identity (`fs.File.size`), matching the qualified `sinfo->name` + // the call sites build. `self`/return types were requalified by + // bindDeclTypes, so validation compares against this too. + const std::string qself = qualifyTypeName(s->modulePath, s->Name); for (auto &meth : s->Methods) { // `cfn` methods (drop / default / at / setAt / len) are // compiler-synthesized hooks that may be invoked from OTHER @@ -865,7 +1059,7 @@ static int compileAndRun(const std::string &filename, return 1; } std::string retStruct = resolveStructName(meth->ReturnType); - if (retStruct != s->Name) { + if (retStruct != qself) { std::cerr << filename << ": error: cfn `default` on struct `" << s->Name << "` must return `Self` (got `" @@ -882,7 +1076,7 @@ static int compileAndRun(const std::string &filename, } std::string selfStruct = resolveStructName(meth->Args[0].Type); - if (selfStruct != s->Name) { + if (selfStruct != qself) { std::cerr << filename << ": error: cfn `" << meth->Name << "` on struct `" << s->Name << "` has self type `" << selfStruct @@ -897,7 +1091,7 @@ static int compileAndRun(const std::string &filename, codegenCtx.getStringPool()); jirDeclarePrototype(jfn, codegenCtx); } - codegenCtx.registerFunctionAST(s->Name + "." + meth->Name, + codegenCtx.registerFunctionAST(qself + "." + meth->Name, meth.get()); } } @@ -1051,6 +1245,9 @@ static int compileAndRun(const std::string &filename, // LLVM-verifier crashes much later. Diagnostics are aborts: the // function shouldn't reach jir_codegen if it's malformed. for (const JirFunction &jfn : jirFunctions) { + // Resolve this body's bare type references against its own + // module (see JirFunction::modulePath). + codegenCtx.pushBodyModule(jfn.modulePath); auto diags = verifyJirFunction( jfn, &codegenCtx.getTypePool(), &codegenCtx.getStringPool(), +[](void *c, TypeIdx t) -> TypeIdx { @@ -1066,6 +1263,7 @@ static int compileAndRun(const std::string &filename, return t; }, &codegenCtx); + codegenCtx.popBodyModule(); for (auto &d : diags) { // jir_verify leaves file empty so the caller can stamp // the unit's currentFile here. @@ -1091,10 +1289,14 @@ static int compileAndRun(const std::string &filename, auto runAnalysisIn = [&](FunctionAST *function, const std::string &file) { if (function->isExtern) return; + // Resolve body-level type references against the function's + // own module while analysis runs. + codegenCtx.pushBodyModule(function->modulePath); auto diags = jam::init_analysis::analyze( *function, codegenCtx.getNodeStore(), codegenCtx.getStringPool(), tokens, &fnRegistry, &dropRegistry, &codegenCtx.getTypePool(), &enumVariants, &analysisHooks); + codegenCtx.popBodyModule(); // Funnel each init-analysis diagnostic into the unified // `jam::Diagnostics` channel so they share the same // formatting / ordering as astgen errors. @@ -1149,8 +1351,12 @@ static int compileAndRun(const std::string &filename, // Pass 2b: emit each JirFunction's LLVM body. Every prototype // (main-module fns, struct methods, imported pub fns) was // declared in pass 1a–1c so jir_codegen Call lookups resolve. + // Push the body's module so getLLVMType / typeSize on bare + // body-level type references resolve to the owning module. for (const JirFunction &jfn : jirFunctions) { + codegenCtx.pushBodyModule(jfn.modulePath); jirDefineBody(jfn, codegenCtx); + codegenCtx.popBodyModule(); } } diff --git a/src/module_resolver.cpp b/src/module_resolver.cpp index 5ca3324..6abd2de 100644 --- a/src/module_resolver.cpp +++ b/src/module_resolver.cpp @@ -107,6 +107,11 @@ std::string ModuleResolver::resolve(const std::string &importPath) const { if (importPath == "test") { return importPath; } std::string path = importPath; + // An explicitly file-relative spelling (`./x`, `../x`) names a file + // near the importer and nothing else — it must not fall through to + // the standard-library lookups below. + bool explicitRelative = + path.rfind("./", 0) == 0 || path.rfind("../", 0) == 0; if (path.substr(0, 2) == "./") { path = path.substr(2); } fs::path base(baseDir); @@ -120,6 +125,8 @@ std::string ModuleResolver::resolve(const std::string &importPath) const { return fs::canonical(indexPath).string(); } + if (explicitRelative) { return ""; } // never std for ./ or ../ + // Standard-library lookup. Accept both `import("collections")` and // `import("std/collections")` spellings by stripping a leading // `std/` so the bare module name resolves under the std root. @@ -147,44 +154,100 @@ std::string ModuleResolver::resolve(const std::string &importPath) const { return ""; // Not found } +namespace { + +// Strip the `.jam` extension and collapse a `/mod` index file to +// `` — but only when no sibling `.jam` exists under `root`. +// `resolve` prefers the direct file, so when both exist they are two +// distinct modules and must keep distinct identities. +std::string finishIdentity(std::string id, const fs::path &rootAbs) { + static const std::string ext = ".jam"; + if (id.size() > ext.size() && + id.compare(id.size() - ext.size(), ext.size(), ext) == 0) { + id.resize(id.size() - ext.size()); + } + static const std::string modSuffix = "/mod"; + if (id.size() > modSuffix.size() && + id.compare(id.size() - modSuffix.size(), modSuffix.size(), modSuffix) == + 0) { + std::string collapsed = id.substr(0, id.size() - modSuffix.size()); + std::error_code ec; + if (!fs::exists(rootAbs / (collapsed + ext), ec)) { + id = std::move(collapsed); + } + } + return id; +} + +// Identity of `resolvedFile` relative to `rootAbs`, or "" when the file +// is not under that root. +std::string identityUnder(const std::string &resolvedFile, + const fs::path &rootAbs) { + std::error_code ec; + fs::path rel = fs::relative(resolvedFile, rootAbs, ec); + if (ec || rel.empty()) return ""; + std::string id = rel.generic_string(); + if (id.rfind("..", 0) == 0 || id == ".") return ""; + return finishIdentity(std::move(id), rootAbs); +} + +} // namespace + std::string ModuleResolver::moduleIdentity(const std::string &resolvedFile) const { // Map a resolved (canonical) module file to the stable identity used // as both the `loadedModules` cache key and the `modulePath` - // mangling prefix: the path relative to the entry base dir, with the - // `.jam` extension stripped and forward slashes. It is the - // project-root-relative path that names a module regardless of which - // relative spelling reached it, so `import("lib/b")` and - // `import("./b")` from `lib/a` agree on the identity `lib/b`. - // - // Returns "" when the file sits outside the base dir — std-library - // modules resolve under their own root, and the caller keeps the - // original import spelling, which `resolve` already maps through the - // std root. + // mangling prefix, so the same file reached via different spellings + // is one module. Three tiers: + // 1. Under the entry base dir: the entry-relative path, `.jam` + // stripped (`lib/b`). `import("lib/b")` and `import("./b")` + // from `lib/a` agree on `lib/b`. + // 2. Under the standard-library root (installed or the in-tree + // dev `std/` fallback): `std/`. `import("fs")` and + // `import("std/fs")` agree on `std/fs` — without this the std + // file loads twice and registers its types under two + // identities. + // 3. Anywhere else: the canonical absolute path, `.jam` stripped. + // Out-of-tree imports still converge per file. (The reference + // compiler rejects these outright; jam permits them.) std::error_code ec; fs::path baseAbs = fs::canonical(baseDir, ec); - if (ec) return ""; - fs::path rel = fs::relative(resolvedFile, baseAbs, ec); - if (ec || rel.empty()) return ""; - std::string id = rel.generic_string(); - if (id.rfind("..", 0) == 0) return ""; // escapes the base dir + if (!ec) { + std::string id = identityUnder(resolvedFile, baseAbs); + if (!id.empty()) return id; + } + if (const auto &root = stdRoot(); root) { + fs::path stdAbs = fs::canonical(*root, ec); + if (!ec) { + std::string id = identityUnder(resolvedFile, stdAbs); + if (!id.empty()) return "std/" + id; + } + } + // Dev fallback root (`/std`), mirroring resolve()'s last + // lookup tier. + fs::path devAbs = fs::canonical("std", ec); + if (!ec) { + std::string id = identityUnder(resolvedFile, devAbs); + if (!id.empty()) return "std/" + id; + } + fs::path abs = fs::path(resolvedFile); + std::string id = abs.generic_string(); static const std::string ext = ".jam"; if (id.size() > ext.size() && id.compare(id.size() - ext.size(), ext.size(), ext) == 0) { id.resize(id.size() - ext.size()); } - // A `/mod.jam` index file shares its directory's identity, so - // `import("foo")` and `import("foo/mod")` name one module — matching - // `resolve`, which maps a bare `import("foo")` to `foo/mod.jam`. - static const std::string modSuffix = "/mod"; - if (id.size() > modSuffix.size() && - id.compare(id.size() - modSuffix.size(), modSuffix.size(), - modSuffix) == 0) { - id.resize(id.size() - modSuffix.size()); - } return id; } +std::string ModuleResolver::canonicalKey(const std::string &importPath) const { + if (importPath == "test") return importPath; + std::string resolved = resolve(importPath); + if (resolved.empty() || resolved == "test") return importPath; + std::string id = moduleIdentity(resolved); + return id.empty() ? importPath : id; +} + std::string ModuleResolver::readFile(const std::string &path) const { std::ifstream file(path); if (!file.is_open()) { return ""; } @@ -221,6 +284,18 @@ ModuleAST *ModuleResolver::getOrLoadModule(const std::string &importPath) { return loadedModules[importPath].get(); } + // Key the cache by the file's canonical identity so every spelling + // of the same file shares one module. + std::string key = moduleIdentity(resolvedPath); + if (key.empty()) key = importPath; + return loadModuleAt(key, resolvedPath); +} + +ModuleAST *ModuleResolver::loadModuleAt(const std::string &key, + const std::string &resolvedPath) { + auto it = loadedModules.find(key); + if (it != loadedModules.end()) { return it->second.get(); } + std::string source = readFile(resolvedPath); if (source.empty()) { std::cerr << "Error: Cannot read module file: " << resolvedPath @@ -244,7 +319,7 @@ ModuleAST *ModuleResolver::getOrLoadModule(const std::string &importPath) { // the module in place — by the time codegen / semantic analysis // touches a cyclic-import target, it's complete. ModuleAST *modPtr = module.get(); - loadedModules[importPath] = std::move(module); + loadedModules[key] = std::move(module); // Recursively load both regular imports (`const x = import(...)`) // and destructuring imports (`const { X } = import(...)`). @@ -273,7 +348,11 @@ ModuleAST *ModuleResolver::getOrLoadModule(const std::string &importPath) { if (nestedResolved.empty() || nestedResolved == "test") return; std::string id = moduleIdentity(nestedResolved); if (!id.empty()) importPath = id; - getOrLoadModule(importPath); + // Load the file the nested resolver actually found. Re-resolving + // the spelling from the entry dir could bind a DIFFERENT file — + // e.g. a user `fmt.jam` beside the entry silently hijacking the + // std library's internal `import("fmt")`. + loadModuleAt(importPath, nestedResolved); }; for (auto &import : modPtr->Imports) { loadNested(import->Path); } for (auto &destImport : modPtr->DestructuringImports) { @@ -293,15 +372,26 @@ ModuleAST *ModuleResolver::getOrLoadModule(const std::string &importPath) { // asked for that exact symbol to be visible to C callers. for (auto &fn : modPtr->Functions) { if (fn->isExtern || fn->isExport) continue; - fn->modulePath = importPath; + fn->modulePath = key; } for (auto &s : modPtr->Structs) { for (auto &m : s->Methods) { if (m->isExtern || m->isExport) continue; - m->modulePath = importPath; + m->modulePath = key; } } + // Stamp the module path on every type declaration too, so a struct/ + // enum/union gets a qualified identity (`lib/b.Thing`) and two + // modules that each define `pub const Thing = struct {…}` no longer + // collide in the global by-name type registries. The entry module + // (parsed directly in main.cpp, never via this resolver) keeps an + // empty modulePath, so its types stay bare-named. + for (auto &s : modPtr->Structs) { s->modulePath = key; } + for (auto &e : modPtr->Enums) { e->modulePath = key; } + for (auto &u : modPtr->Unions) { u->modulePath = key; } + for (auto &c : modPtr->Consts) { c->modulePath = key; } + return modPtr; } diff --git a/src/module_resolver.h b/src/module_resolver.h index 02ead32..a65ea29 100644 --- a/src/module_resolver.h +++ b/src/module_resolver.h @@ -26,6 +26,14 @@ class ModuleResolver { std::string resolve(const std::string &importPath) const; + // Canonical cache key for an import spelling: resolve it, then map + // the file to its stable identity (entry-relative, `std/` for + // standard-library files, or the absolute path for out-of-tree + // files). Distinct spellings of one file agree on the key; returns + // the spelling unchanged when resolution fails (the load will then + // report the error) or for the `test` builtin. + std::string canonicalKey(const std::string &importPath) const; + ModuleAST *getOrLoadModule(const std::string &importPath); bool isLoaded(const std::string &importPath) const; @@ -51,11 +59,20 @@ class ModuleResolver { std::vector> *sharedAnonEnums_ = nullptr; std::unordered_map> loadedModules; - // Canonical entry-relative identity for a resolved module file, used - // as the cache key and `modulePath` prefix. Empty when the file lies - // outside the base dir (e.g. std-library modules). + // Canonical identity for a resolved module file, used as the cache + // key and `modulePath` prefix: entry-relative for in-tree files, + // `std/` for standard-library files, the absolute path for + // out-of-tree files. Empty only when canonicalization fails. std::string moduleIdentity(const std::string &resolvedFile) const; + // Load (or return the cached) module for `key`, reading from + // `resolvedPath` — the file the caller already resolved. Never + // re-resolves: a nested import loads the file its own module's + // resolver found, not whatever the entry dir happens to shadow it + // with. + ModuleAST *loadModuleAt(const std::string &key, + const std::string &resolvedPath); + std::string readFile(const std::string &path) const; std::unique_ptr parseSource(const std::string &source) const; diff --git a/src/parser.cpp b/src/parser.cpp index b514550..6c35366 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -177,9 +177,7 @@ NodeIdx Parser::parsePrimary() { // `match (…) { … }` is also valid in expression position so it can // produce a value. The same call works for both statement and // expression forms; the codegen builds a phi over arm values. - if (check(TOK_MATCH)) { - return parseMatch(); - } + if (check(TOK_MATCH)) { return parseMatch(); } // `@name(arg, ...)` — compiler intrinsic invocation. Two encoding // shapes: @@ -882,10 +880,21 @@ NodeIdx Parser::parsePatternAtom() { advance(); // consume enum name if (match(TOK_DOT)) { consume(TOK_IDENTIFIER, "Expected variant name after `.`"); - StringIdx enumNameId = - stringPool->intern(tokens[saved].text(source_)); - StringIdx variantNameId = - stringPool->intern(previous().text(source_)); + // Handle-qualified receivers: `a.Status.Ok` parses as + // receiver `a.Status` + variant `Ok` — every segment before + // the last joins the receiver chain (the lowering resolves + // it through the import-handle namespace), the last names + // the variant. + std::string recvName(tokens[saved].text(source_)); + std::string lastSeg(previous().text(source_)); + while (match(TOK_DOT)) { + consume(TOK_IDENTIFIER, "Expected variant name after `.`"); + recvName += "."; + recvName += lastSeg; + lastSeg = std::string(previous().text(source_)); + } + StringIdx enumNameId = stringPool->intern(recvName); + StringIdx variantNameId = stringPool->intern(lastSeg); // Optional payload binding: `(name1, name2, ...)`. Empty // list `()` is permitted and equivalent to no parens. diff --git a/src/target.cpp b/src/target.cpp index 9a69ce5..51765f4 100644 --- a/src/target.cpp +++ b/src/target.cpp @@ -303,4 +303,4 @@ Target::CallingConvention Target::getDefaultCC() const { return CallingConvention::C; } -} +} // namespace jam diff --git a/src/target.h b/src/target.h index 721e4ea..ff29f51 100644 --- a/src/target.h +++ b/src/target.h @@ -65,6 +65,6 @@ struct Target { CallingConvention getDefaultCC() const; }; -} +} // namespace jam #endif diff --git a/src/token.h b/src/token.h index b9cec97..cdc327d 100644 --- a/src/token.h +++ b/src/token.h @@ -64,22 +64,22 @@ enum TokenType { TOK_STRUCT, TOK_UNION, TOK_ENUM, - TOK_STAR, // * (pointer prefix; multiplication) - TOK_SLASH, // / (division) - TOK_PERCENT, // % (modulo) - TOK_AMP, // & (bitwise AND; address-of) - TOK_PIPE, // | (bitwise OR) - TOK_CARET, // ^ (bitwise XOR) - TOK_TILDE, // ~ (bitwise NOT) - TOK_LSHIFT, // << (left shift) - TOK_RSHIFT, // >> (right shift) + TOK_STAR, // * (pointer prefix; multiplication) + TOK_SLASH, // / (division) + TOK_PERCENT, // % (modulo) + TOK_AMP, // & (bitwise AND; address-of) + TOK_PIPE, // | (bitwise OR) + TOK_CARET, // ^ (bitwise XOR) + TOK_TILDE, // ~ (bitwise NOT) + TOK_LSHIFT, // << (left shift) + TOK_RSHIFT, // >> (right shift) TOK_MOVE, - TOK_ELLIPSIS, // ... (variadic marker in extern fn parameters) + TOK_ELLIPSIS, // ... (variadic marker in extern fn parameters) TOK_MATCH, TOK_DOTDOT, // .. (exclusive range / slice) TOK_DOTDOT_EQ, // ..= (inclusive range in match patterns) TOK_AS, - TOK_AT, // @ + TOK_AT, // @ TOK_COMP, TOK_INLINE, }; diff --git a/tests/cpp/test_codegen_errors.cpp b/tests/cpp/test_codegen_errors.cpp index a1ca6d0..49dec5a 100644 --- a/tests/cpp/test_codegen_errors.cpp +++ b/tests/cpp/test_codegen_errors.cpp @@ -1510,7 +1510,8 @@ fn main() {} )"); ASSERT_TRUE(idx.exitCode != 0); ASSERT_TRUE(stderrContains( - idx, "`Vec__Counter.at` is not available for this instantiation")); + idx, "`std/collections.Vec__Counter.at` is not available for " + "this instantiation")); ASSERT_TRUE(stderrContains(idx, "owns resources")); auto filled = compileSource("cond_filled_replay", prelude + R"( @@ -1522,8 +1523,8 @@ fn main() {} )"); ASSERT_TRUE(filled.exitCode != 0); ASSERT_TRUE(stderrContains( - filled, - "`Vec__Counter.filled` is not available for this instantiation")); + filled, "`std/collections.Vec__Counter.filled` is not available " + "for this instantiation")); ASSERT_TRUE(stderrContains(filled, "borrowed, not owned")); auto get = compileSource("cond_get_replay", prelude + R"( @@ -1537,7 +1538,8 @@ fn main() {} )"); ASSERT_TRUE(get.exitCode != 0); ASSERT_TRUE(stderrContains( - get, "`Vec__Counter.get` is not available for this instantiation")); + get, "`std/collections.Vec__Counter.get` is not available for " + "this instantiation")); } // MATCH-MOVE rejections: matching a drop-bearing enum consumes the diff --git a/tests/unit/import_resolution/conv_a.jam b/tests/unit/import_resolution/conv_a.jam new file mode 100644 index 0000000..6f332d0 --- /dev/null +++ b/tests/unit/import_resolution/conv_a.jam @@ -0,0 +1,6 @@ +// Imports conv_b by its bare spelling; the entry uses `./conv_b`. +const { Thing, make } = import("conv_b"); + +pub fn makeViaA() Thing { + return make(); +} diff --git a/tests/unit/import_resolution/conv_b.jam b/tests/unit/import_resolution/conv_b.jam new file mode 100644 index 0000000..4a57eda --- /dev/null +++ b/tests/unit/import_resolution/conv_b.jam @@ -0,0 +1,10 @@ +// Helper for test_entry_relative_convergence — the module both +// spellings (`./conv_b` from the entry, `conv_b` from conv_a) must +// agree on. +pub const Thing = struct { + value: u64, +}; + +pub fn make() Thing { + return Thing { value: 5 }; +} diff --git a/tests/unit/import_resolution/fmt.jam b/tests/unit/import_resolution/fmt.jam new file mode 100644 index 0000000..3eb6fc4 --- /dev/null +++ b/tests/unit/import_resolution/fmt.jam @@ -0,0 +1,8 @@ +// A user file deliberately named after a std module (`fmt`). It must +// NOT replace the std library's fmt inside std's own imports — only +// the user's own `import("fmt")` binds here. Defines no `print`, so if +// std's internal `import("fmt")` ever mis-resolved to this file, +// `std.fmt.print` would fail to compile. +pub fn userFmtMarker() u64 { + return 77; +} diff --git a/tests/unit/import_resolution/modfile.jam b/tests/unit/import_resolution/modfile.jam new file mode 100644 index 0000000..eaca3d1 --- /dev/null +++ b/tests/unit/import_resolution/modfile.jam @@ -0,0 +1,6 @@ +// Direct file that shares its name with a directory module +// (modfile/mod.jam). The two are distinct modules: `import("modfile")` +// binds this file, `import("modfile/mod")` binds the directory index. +pub fn which() u64 { + return 1; +} diff --git a/tests/unit/import_resolution/modfile/mod.jam b/tests/unit/import_resolution/modfile/mod.jam new file mode 100644 index 0000000..3a8f670 --- /dev/null +++ b/tests/unit/import_resolution/modfile/mod.jam @@ -0,0 +1,5 @@ +// Directory-index module whose directory name collides with a direct +// file (modfile.jam). See modfile.jam. +pub fn which() u64 { + return 2; +} diff --git a/tests/unit/import_resolution/nest_dir.jam b/tests/unit/import_resolution/nest_dir.jam new file mode 100644 index 0000000..81b0a78 --- /dev/null +++ b/tests/unit/import_resolution/nest_dir.jam @@ -0,0 +1,8 @@ +// Imports the directory-index module from a NESTED module, exercising +// the nested-resolution path where the identity collapse of +// `/mod` once swallowed the distinct sibling modfile.jam. +const fm = import("modfile/mod"); + +pub fn viaNested() u64 { + return fm.which(); +} diff --git a/tests/unit/import_resolution/test_dir_index.jam b/tests/unit/import_resolution/test_dir_index.jam new file mode 100644 index 0000000..7018be9 --- /dev/null +++ b/tests/unit/import_resolution/test_dir_index.jam @@ -0,0 +1,14 @@ +// modfile.jam vs modfile/mod.jam: a direct file and a directory-index +// module sharing one name are two distinct modules. The identity +// collapse of `/mod` to `` is guarded on the direct file's +// absence — when both exist, a nested `import("modfile/mod")` once +// silently bound modfile.jam instead. + +const { assert } = import("test"); +const f = import("modfile"); +const nested = import("nest_dir"); + +tfn directFileAndDirIndexAreDistinct() { + assert(f.which(), 1); + assert(nested.viaNested(), 2); +} diff --git a/tests/unit/import_resolution/test_entry_relative_convergence.jam b/tests/unit/import_resolution/test_entry_relative_convergence.jam new file mode 100644 index 0000000..038b27e --- /dev/null +++ b/tests/unit/import_resolution/test_entry_relative_convergence.jam @@ -0,0 +1,15 @@ +// Entry-module `./conv_b` and a nested module's `conv_b` are two +// spellings of one file and must share one module identity. Entry +// imports were once cached under their RAW spelling while nested ones +// used the canonical identity — the same file loaded twice, its types +// split into two nominal identities, and values crossing the boundary +// silently mixed them. + +const { assert } = import("test"); +const { Thing } = import("./conv_b"); +const a = import("conv_a"); + +tfn entryRelativeSpellingConvergesWithNested() { + var t: Thing = a.makeViaA(); + assert(t.value, 5); +} diff --git a/tests/unit/import_resolution/test_std_shadow.jam b/tests/unit/import_resolution/test_std_shadow.jam new file mode 100644 index 0000000..44f5aba --- /dev/null +++ b/tests/unit/import_resolution/test_std_shadow.jam @@ -0,0 +1,19 @@ +// A sibling fmt.jam must not hijack the std library's internals. The +// resolver once re-resolved std's internal `import("fmt")` from the +// ENTRY directory — where the user's fmt.jam shadowed the std one — so +// `std.fmt.print` silently dispatched to user code. Nested imports now +// load the exact file their own module's resolver found. +// +// Compiling this file is most of the test: `std.fmt.print` only +// resolves if std's fmt re-export still points at the std library's +// fmt (the user's fmt.jam defines no `print`). The user's module stays +// importable under the same bare name from the entry. + +const { assert } = import("test"); +const std = import("std"); +const userFmt = import("fmt"); + +tfn userFileDoesNotHijackStdInternals() { + std.fmt.print(""); + assert(userFmt.userFmtMarker(), 77); +} diff --git a/tests/unit/mod_alias_holder.jam b/tests/unit/mod_alias_holder.jam new file mode 100644 index 0000000..bdd5624 --- /dev/null +++ b/tests/unit/mod_alias_holder.jam @@ -0,0 +1,18 @@ +// Helper for test_xmod_aliases: a generic, a sibling type, and a type +// alias whose argument names the sibling. The alias's argument must +// capture THIS module's `Inner` even when the importer defines its own +// type of the same name. +pub const Inner = struct { value: i64 }; + +pub fn Holder(T: type) type { + return struct { + inner: T, + pub fn val(self: Self) i64 { return self.inner.value; } + }; +} + +pub const W = Holder(Inner); + +pub fn makeHolder(v: i64) Holder(Inner) { + return Holder(Inner) { inner: Inner { value: v } }; +} diff --git a/tests/unit/mod_comp_a.jam b/tests/unit/mod_comp_a.jam new file mode 100644 index 0000000..0d93bf4 --- /dev/null +++ b/tests/unit/mod_comp_a.jam @@ -0,0 +1,11 @@ +// Helper for test_xmod_comp_fns — mod_comp_b defines a same-named +// comp-param fn with a different body. The per-call-site clone cache +// keys by the callee's qualified identity, so the two never share a +// clone (or an LLVM symbol). +pub fn scale(comp k: u32, x: u32) u32 { + return x * k; +} + +pub fn runA() u32 { + return scale(2, 10); +} diff --git a/tests/unit/mod_comp_b.jam b/tests/unit/mod_comp_b.jam new file mode 100644 index 0000000..02aa767 --- /dev/null +++ b/tests/unit/mod_comp_b.jam @@ -0,0 +1,8 @@ +// Helper for test_xmod_comp_fns — see mod_comp_a. +pub fn scale(comp k: u32, x: u32) u32 { + return x + k; +} + +pub fn runB() u32 { + return scale(2, 10); +} diff --git a/tests/unit/mod_const_a.jam b/tests/unit/mod_const_a.jam new file mode 100644 index 0000000..ab91dd7 --- /dev/null +++ b/tests/unit/mod_const_a.jam @@ -0,0 +1,21 @@ +// Helper for test_xmod_consts — mod_const_b defines same-named consts +// with different values. Module consts live in a per-module namespace: +// each body (and each comptime fold, like the array length below) must +// read ITS module's values. +pub const N: u32 = 4; +comp const SIZE = 3; + +pub fn readN() u32 { + return N; +} + +pub fn lastSlot() u8 { + var buf: [SIZE]u8 = [0; SIZE]; + buf[SIZE - 1] = 42; + return buf[SIZE - 1]; +} + +pub fn sizeOfBuf() u64 { + var buf: [SIZE]u8 = [0; SIZE]; + return @sizeOf([SIZE]u8); +} diff --git a/tests/unit/mod_const_b.jam b/tests/unit/mod_const_b.jam new file mode 100644 index 0000000..226ae82 --- /dev/null +++ b/tests/unit/mod_const_b.jam @@ -0,0 +1,18 @@ +// Helper for test_xmod_consts — see mod_const_a. +pub const N: u32 = 9; +comp const SIZE = 5; + +pub fn readN() u32 { + return N; +} + +pub fn lastSlot() u8 { + var buf: [SIZE]u8 = [0; SIZE]; + buf[SIZE - 1] = 42; + return buf[SIZE - 1]; +} + +pub fn sizeOfBuf() u64 { + var buf: [SIZE]u8 = [0; SIZE]; + return @sizeOf([SIZE]u8); +} diff --git a/tests/unit/mod_gen_a.jam b/tests/unit/mod_gen_a.jam new file mode 100644 index 0000000..bffdfee --- /dev/null +++ b/tests/unit/mod_gen_a.jam @@ -0,0 +1,15 @@ +// Helper for test_xmod_generics: a generic factory named `Pair` — +// mod_gen_b defines a DIFFERENT `Pair` (other field name, other method +// behavior). Generic identity is module-qualified, so the two must +// never share an instantiation. +pub fn Pair(T: type) type { + return struct { + x: T, + pub fn val(self: Self) T { return self.x * 2; } + }; +} + +pub fn makeA() u64 { + var p: Pair(u64) = Pair(u64) { x: 9 }; + return p.val(); +} diff --git a/tests/unit/mod_gen_b.jam b/tests/unit/mod_gen_b.jam new file mode 100644 index 0000000..d23d616 --- /dev/null +++ b/tests/unit/mod_gen_b.jam @@ -0,0 +1,14 @@ +// Helper for test_xmod_generics — see mod_gen_a. Different field name +// AND different method behavior, so a shared instantiation fails +// loudly (unknown field) or wrongly (bad value). +pub fn Pair(T: type) type { + return struct { + y: T, + pub fn val(self: Self) T { return self.y + 1000; } + }; +} + +pub fn makeB() u64 { + var p: Pair(u64) = Pair(u64) { y: 9 }; + return p.val(); +} diff --git a/tests/unit/mod_gen_sibling.jam b/tests/unit/mod_gen_sibling.jam new file mode 100644 index 0000000..09e89c2 --- /dev/null +++ b/tests/unit/mod_gen_sibling.jam @@ -0,0 +1,34 @@ +// Helper for test_generic_sibling_types: generics whose anon bodies +// reference a SIBLING type (`Inner`) in field, method-signature, and +// enum-payload positions. Those references must resolve against THIS +// module — not whatever `Inner` means at the instantiation site. +pub const Inner = struct { + value: i64, +}; + +pub fn Holder(T: type) type { + return struct { + inner: Inner, + item: T, + pub fn innerValue(self: Self) i64 { return self.inner.value; } + pub fn makeInner(self: Self) Inner { + return Inner { value: self.inner.value + 1 }; + } + }; +} + +pub fn makeHolder(v: i64, x: i32) Holder(i32) { + return Holder(i32) { inner: Inner { value: v }, item: x }; +} + +pub fn MaybeInner(T: type) type { + return enum { + Nothing, + Has(Inner), + Item(T), + }; +} + +pub fn wrapInner(v: i64) MaybeInner(u8) { + return MaybeInner(u8).Has(Inner { value: v }); +} diff --git a/tests/unit/mod_samename_a.jam b/tests/unit/mod_samename_a.jam new file mode 100644 index 0000000..a2bd8c5 --- /dev/null +++ b/tests/unit/mod_samename_a.jam @@ -0,0 +1,22 @@ +// Helper for test_samename_struct_collision: defines `Pair` and `Wrap`. +// Another module (mod_samename_b) defines a DIFFERENT `Pair`; the two +// must stay distinct types under module-qualified identity. +pub const Pair = struct { + a: u64, +}; + +pub const Wrap = struct { + inner: Pair, +}; + +pub fn mk() Pair { + return Pair { a: 11 }; +} + +pub fn wrapped() Wrap { + return Wrap { inner: Pair { a: 11 } }; +} + +pub fn innerA(w: Wrap) u64 { + return w.inner.a; +} diff --git a/tests/unit/mod_samename_b.jam b/tests/unit/mod_samename_b.jam new file mode 100644 index 0000000..9d18976 --- /dev/null +++ b/tests/unit/mod_samename_b.jam @@ -0,0 +1,14 @@ +// Helper for test_samename_struct_collision: defines a `Pair` whose +// layout differs from mod_samename_a's `Pair` (two fields, not one). +pub const Pair = struct { + a: u64, + b: u64, +}; + +pub fn mk() Pair { + return Pair { a: 20, b: 22 }; +} + +pub fn sum(p: Pair) u64 { + return p.a + p.b; +} diff --git a/tests/unit/mod_status.jam b/tests/unit/mod_status.jam new file mode 100644 index 0000000..6c543bb --- /dev/null +++ b/tests/unit/mod_status.jam @@ -0,0 +1,14 @@ +// Helper for test_enum_handle_access: a payloaded enum used through an +// import handle. +pub const Status = enum { + Ok, + Bad(u32), +}; + +pub fn describe(s: Status) u32 { + match (s) { + Status.Ok { return 0; } + Status.Bad(c) { return c; } + } + return 99; +} diff --git a/tests/unit/mod_std_two_spellings.jam b/tests/unit/mod_std_two_spellings.jam new file mode 100644 index 0000000..3ecfe56 --- /dev/null +++ b/tests/unit/mod_std_two_spellings.jam @@ -0,0 +1,13 @@ +// Helper for test_std_two_spellings: imports a std module by its BARE +// spelling while the entry test uses the `std/` spelling. Both must +// canonicalize to one module identity (`std/collections`) — the same +// file once loaded twice under two keys, duplicating every type and +// function it defines. +const { Vec } = import("collections"); + +pub fn makeVec() Vec(u8) { + var v: Vec(u8) = Vec(u8).empty(); + v.push(1); + v.push(2); + return v; +} diff --git a/tests/unit/mod_xmod_clone.jam b/tests/unit/mod_xmod_clone.jam new file mode 100644 index 0000000..15d4536 --- /dev/null +++ b/tests/unit/mod_xmod_clone.jam @@ -0,0 +1,18 @@ +// Helper for test_xmod_clone: a drop-bearing struct with a user cfn +// clone, owned by a non-entry module. Note the type is deliberately +// named `Token` — mod_xmod_moves also defines a `Token`; the two must +// stay distinct under qualified identity. +pub const Token = struct { + sink: *mut u32, + cfn drop(self: mut Token) { + var p: *mut u32 = self.sink; + p.* = p.* + 1; + } + cfn clone(self: Token) Token { + return Token { sink: self.sink }; + } +}; + +pub fn makeToken(s: *mut u32) Token { + return Token { sink: s }; +} diff --git a/tests/unit/mod_xmod_enum.jam b/tests/unit/mod_xmod_enum.jam new file mode 100644 index 0000000..1324440 --- /dev/null +++ b/tests/unit/mod_xmod_enum.jam @@ -0,0 +1,31 @@ +// Helper for test_xmod_enum: a unit enum and a payloaded enum owned by +// a non-entry module. Regression for the qualified-identity scheme: +// enum registries key by the qualified name, and every variant +// reference site (member access, match patterns, the lvalue +// classifier) must requalify before lookup — including uses inside +// this module's OWN bodies. +pub const Signal = enum { Stop, Go, Wait }; + +pub fn pick() Signal { + var s: Signal = Signal.Go; + return s; +} + +pub fn classify(s: Signal) u64 { + match (s) { + Signal.Stop { return 1; } + Signal.Go { return 2; } + Signal.Wait { return 3; } + } + return 0; +} + +pub const Box = enum { Empty, Val(u64) }; + +pub fn classifyBox(b: Box) u64 { + match (b) { + Box.Empty { return 0; } + Box.Val(v) { return v; } + } + return 99; +} diff --git a/tests/unit/mod_xmod_fielddrop.jam b/tests/unit/mod_xmod_fielddrop.jam new file mode 100644 index 0000000..720be47 --- /dev/null +++ b/tests/unit/mod_xmod_fielddrop.jam @@ -0,0 +1,20 @@ +// Helper for test_xmod_fielddrop: a drop-bearing struct and an +// aggregate holding it as a FIELD (no own drop). The aggregate's +// drop-bearing-ness is derived transitively from its fields, so the +// classification sites must resolve this module's structs by their +// qualified identity. +pub const XThing = struct { + sink: *mut u32, + cfn drop(self: mut XThing) { + var p: *mut u32 = self.sink; + p.* = p.* + 1; + } +}; + +pub const Outer = struct { + thing: XThing, +}; + +pub fn makeOuter(s: *mut u32) Outer { + return Outer { thing: XThing { sink: s } }; +} diff --git a/tests/unit/mod_xmod_moves.jam b/tests/unit/mod_xmod_moves.jam new file mode 100644 index 0000000..c481a00 --- /dev/null +++ b/tests/unit/mod_xmod_moves.jam @@ -0,0 +1,27 @@ +// Helper for test_xmod_moves: a drop-bearing struct plus a struct +// whose method takes a move-mode parameter of it. Move tracking for +// these from the entry module depends on the analyzer resolving the +// imported types' qualified identities (drop registry keys and the +// `Type.method` mode-lookup keys are both qualified). +pub const Token = struct { + sink: *mut u32, + cfn drop(self: mut Token) { + var p: *mut u32 = self.sink; + p.* = p.* + 1; + } +}; + +pub fn makeToken(s: *mut u32) Token { + return Token { sink: s }; +} + +pub const Eater = struct { + pad: u32, + pub fn eat(self: Eater, t: move Token) u64 { + return 1; + } +}; + +pub fn makeEater() Eater { + return Eater { pad: 7 }; +} diff --git a/tests/unit/test_enum_handle_access.jam b/tests/unit/test_enum_handle_access.jam new file mode 100644 index 0000000..f3ecfcc --- /dev/null +++ b/tests/unit/test_enum_handle_access.jam @@ -0,0 +1,25 @@ +// Handle-qualified enum access: `a.Status.Bad(5)` construction, +// `a.Status.Ok` unit-variant expressions, and `a.Status.Variant` match +// patterns. None of these had a working spelling before: the multi-dot +// call path covered struct methods only, the member-access path only +// bare Variable receivers, and the pattern grammar stopped at one dot. + +const { assert } = import("test"); +const a = import("mod_status"); + +tfn handleQualifiedPayloadCtor() { + assert(a.describe(a.Status.Bad(5)), 5); +} + +tfn handleQualifiedUnitVariantExpr() { + var s: a.Status = a.Status.Ok; + assert(a.describe(s), 0); +} + +tfn handleQualifiedPattern() { + var s: a.Status = a.Status.Bad(7); + match (s) { + a.Status.Ok { assert(1, 0); } + a.Status.Bad(c) { assert(c, 7); } + } +} diff --git a/tests/unit/test_generic_arg_spellings.jam b/tests/unit/test_generic_arg_spellings.jam new file mode 100644 index 0000000..9127f58 --- /dev/null +++ b/tests/unit/test_generic_arg_spellings.jam @@ -0,0 +1,29 @@ +// Generic instantiations memoize on a name built from the argument +// types, so every distinct argument type must spell distinctly. The +// spelling once fell back to a shared catch-all for floats (and +// pointers/slices/arrays), handing `Pair(f64)` the struct layout +// already instantiated for `Pair(f32)` — silent value corruption. + +const { assert } = import("test"); + +fn Pair(T: type) type { + return struct { + a: T, + b: T, + pub fn sum(self: Self) T { return self.a + self.b; } + }; +} + +tfn floatInstantiationsStayDistinct() { + var p32: Pair(f32) = Pair(f32) { a: 1.5, b: 2.5 }; + var p64: Pair(f64) = Pair(f64) { a: 1.25, b: 2.25 }; + assert((p32.sum() == 4.0) as u8, 1); + assert((p64.sum() == 3.5) as u8, 1); +} + +tfn intAndFloatInstantiationsCoexist() { + var pi: Pair(u32) = Pair(u32) { a: 3, b: 4 }; + var pf: Pair(f64) = Pair(f64) { a: 0.5, b: 0.25 }; + assert(pi.sum(), 7); + assert((pf.sum() == 0.75) as u8, 1); +} diff --git a/tests/unit/test_generic_sibling_types.jam b/tests/unit/test_generic_sibling_types.jam new file mode 100644 index 0000000..6bf1e42 --- /dev/null +++ b/tests/unit/test_generic_sibling_types.jam @@ -0,0 +1,44 @@ +// A generic's anon body referencing its module's sibling types. These +// references once resolved against the INSTANTIATING module (dynamic +// scoping): instantiating from a module that didn't import `Inner` +// failed with "Unknown user-defined type", and one that defined its +// OWN `Inner` silently captured the wrong layout. Substituted field, +// signature, and enum-payload types now qualify against the generic's +// defining module. +// +// Note: this entry module deliberately defines its own unrelated +// `Inner` and never imports the helper's. + +const { assert } = import("test"); +const m = import("mod_gen_sibling"); +const { MaybeInner, wrapInner } = import("mod_gen_sibling"); + +const Inner = struct { + flag: bool, +}; + +tfn siblingFieldResolvesInDefiningModule() { + var h: m.Holder(i32) = m.makeHolder(41, 7); + assert(h.innerValue() as u64, 41); +} + +tfn siblingSignatureResolvesInDefiningModule() { + var h: m.Holder(i32) = m.makeHolder(10, 1); + assert(h.makeInner().value as u64, 11); +} + +tfn entryOwnSameNamedTypeNotCaptured() { + var h: m.Holder(i32) = m.makeHolder(123, 1); + assert(h.innerValue() as u64, 123); + var mine: Inner = Inner { flag: true }; + assert(mine.flag as u64, 1); +} + +tfn genericEnumSiblingPayloadResolves() { + var w: MaybeInner(u8) = wrapInner(55); + match (w) { + MaybeInner(u8).Has(i) { assert(i.value as u64, 55); } + MaybeInner(u8).Item(x) { assert(x as u64, 0); } + _ { assert(1, 0); } + } +} diff --git a/tests/unit/test_samename_struct_collision.jam b/tests/unit/test_samename_struct_collision.jam new file mode 100644 index 0000000..27a2fb9 --- /dev/null +++ b/tests/unit/test_samename_struct_collision.jam @@ -0,0 +1,36 @@ +// Bug B regression: two modules each define `pub const Pair = struct {…}` +// with DIFFERENT fields. Before types carried a module-qualified identity, +// both `Pair`s collided on the bare name in the global type registries — +// the second module's `Pair { a, b }` literal failed to compile with +// "unknown struct field `b`", and a struct value built in one module could +// be laid out with the other's fields. Types are now keyed by +// `.Pair`, so the two are distinct. + +const { assert } = import("test"); +const A = import("mod_samename_a"); +const B = import("mod_samename_b"); +const { Pair } = import("mod_samename_b"); + +// Distinct layouts resolve correctly through import handles: A.Pair has +// one field, B.Pair has two, and each module's own `mk()` body builds the +// right one. +tfn sameNamedStructsAcrossModulesAreDistinct() { + var pb: B.Pair = B.mk(); + assert(A.mk().a, 11); + assert(B.sum(pb), 42); +} + +// A module-local struct field whose type is itself a same-named type lays +// out and reads back correctly — exercises declaration-level field +// qualification and the analyzer's layout path. +tfn moduleLocalFieldOfSameNamedTypeLaysOut() { + var w: A.Wrap = A.wrapped(); + assert(A.innerA(w), 11); +} + +// A destructuring import binds the bare name `Pair` to B's `Pair`, even +// though A's `Pair` is also in scope via the `A` handle. +tfn destructuredSameNamedTypeResolvesToItsSource() { + var p: Pair = B.mk(); + assert(p.a + p.b, 42); +} diff --git a/tests/unit/test_std_two_spellings.jam b/tests/unit/test_std_two_spellings.jam new file mode 100644 index 0000000..27eccfe --- /dev/null +++ b/tests/unit/test_std_two_spellings.jam @@ -0,0 +1,15 @@ +// One program importing the same std module under both accepted +// spellings — `import("std/collections")` here, `import("collections")` +// in the helper — with a Vec value crossing the boundary. The two +// spellings must resolve to ONE module identity so the value's type is +// the same nominal type on both sides. + +const { assert } = import("test"); +const { Vec } = import("std/collections"); +const h = import("mod_std_two_spellings"); + +tfn twoSpellingsShareOneModule() { + var v: Vec(u8) = h.makeVec(); + v.push(9); + assert(v.len() as i32, 3); +} diff --git a/tests/unit/test_xmod_aliases.jam b/tests/unit/test_xmod_aliases.jam new file mode 100644 index 0000000..fdc39da --- /dev/null +++ b/tests/unit/test_xmod_aliases.jam @@ -0,0 +1,18 @@ +// Type aliases are per-module. The flat alias table was once keyed by +// bare name for every module (entry wins collisions), and an alias's +// generic ARGUMENTS were interned as bare names that requalified +// against the CONSUMER's module — `W = Holder(Inner)` could capture +// the importer's `Inner`. Aliases now register owner-qualified with +// their targets requalified against the owning module. + +const { assert } = import("test"); +const { W, makeHolder } = import("mod_alias_holder"); + +// Entry defines its own unrelated Inner; W must still mean +// Holder(). +const Inner = struct { flag: bool }; + +tfn aliasArgsCaptureOwnersTypes() { + var w: W = makeHolder(77); + assert(w.val() as u64, 77); +} diff --git a/tests/unit/test_xmod_clone.jam b/tests/unit/test_xmod_clone.jam new file mode 100644 index 0000000..38d5e45 --- /dev/null +++ b/tests/unit/test_xmod_clone.jam @@ -0,0 +1,17 @@ +// Cross-module cfn clone: `.clone()` on a value whose clone is defined +// in an imported module must dispatch to the user clone (not degrade +// to a bitwise copy), and both the original and the clone drop. + +const { assert } = import("test"); +const { Token, makeToken } = import("mod_xmod_clone"); + +fn cloneBoth(s: *mut u32) { + var t: Token = makeToken(s); + var c: Token = t.clone(); +} + +tfn importedCloneDispatchesAndBothDrop() { + var hits: u32 = 0; + cloneBoth(&hits); + assert(hits, 2); +} diff --git a/tests/unit/test_xmod_comp_fns.jam b/tests/unit/test_xmod_comp_fns.jam new file mode 100644 index 0000000..0245da6 --- /dev/null +++ b/tests/unit/test_xmod_comp_fns.jam @@ -0,0 +1,15 @@ +// Same-named comp-param fns in two modules. The per-call-site clone +// cache was once keyed by the BARE name + comp-arg suffix +// (`scale__u2`), so both modules shared one cache slot and whichever +// instantiated first answered for both — runA()/runB() silently +// returned the same value. The key now carries the callee's qualified +// identity. + +const { assert } = import("test"); +const a = import("mod_comp_a"); +const b = import("mod_comp_b"); + +tfn sameNamedCompFnsStayDistinct() { + assert(a.runA() as u64, 20); + assert(b.runB() as u64, 12); +} diff --git a/tests/unit/test_xmod_consts.jam b/tests/unit/test_xmod_consts.jam new file mode 100644 index 0000000..28875db --- /dev/null +++ b/tests/unit/test_xmod_consts.jam @@ -0,0 +1,35 @@ +// Module consts are per-module. The const registry and the comptime +// scope it seeds were once ONE flat bare-name map shared by every +// module (entry registered last, so it won every collision): a body +// reading `N` could get another module's value, and a comptime array +// length `[SIZE]u8` folded against whichever module registered last. +// Consts now register under their owner-qualified identity and each +// body's comptime scope seeds only its own module's (+ destructured) +// consts. + +const { assert } = import("test"); +const a = import("mod_const_a"); +const b = import("mod_const_b"); + +// The entry's own same-named consts must not leak into the modules. +comp const N = 77; +comp const SIZE = 11; + +tfn eachModuleReadsItsOwnConst() { + assert(a.readN(), 4); + assert(b.readN(), 9); +} + +tfn comptimeArrayLengthsFoldPerModule() { + assert(a.lastSlot(), 42); + assert(b.lastSlot(), 42); + assert(a.sizeOfBuf(), 3); + assert(b.sizeOfBuf(), 5); +} + +tfn entryReadsItsOwnConsts() { + var n: u32 = N; + var s: u32 = SIZE; + assert(n, 77); + assert(s, 11); +} diff --git a/tests/unit/test_xmod_enum.jam b/tests/unit/test_xmod_enum.jam new file mode 100644 index 0000000..b0b40b8 --- /dev/null +++ b/tests/unit/test_xmod_enum.jam @@ -0,0 +1,40 @@ +// Cross-module enums. Regression: enum registries key by the +// module-qualified identity, but the variant-reference sites in astgen +// (member access for `Enum.Variant`, match-pattern receivers, the +// non-lvalue classifier) once looked up the BARE source name — every +// enum owned by a non-entry module failed with +// `unknown lvalue variable` at its first variant use, even inside the +// defining module's own bodies. + +const { assert } = import("test"); +const { Signal, pick, classify, Box, classifyBox } = import("mod_xmod_enum"); + +tfn definingModuleConstructsOwnVariants() { + assert(classify(pick()), 2); +} + +tfn entryConstructsImportedUnitVariant() { + var s: Signal = Signal.Wait; + assert(classify(s), 3); +} + +tfn entryMatchesImportedEnumByName() { + var s: Signal = Signal.Stop; + match (s) { + Signal.Stop { assert(1, 1); } + _ { assert(1, 0); } + } +} + +tfn entryPassesImportedVariantAsArg() { + assert(classify(Signal.Go), 2); +} + +tfn importedPayloadVariantConstructsAndMatches() { + assert(classifyBox(Box.Val(42)), 42); + var b: Box = Box.Empty; + match (b) { + Box.Empty { assert(1, 1); } + Box.Val(v) { assert(v, 0); } + } +} diff --git a/tests/unit/test_xmod_fielddrop.jam b/tests/unit/test_xmod_fielddrop.jam new file mode 100644 index 0000000..2b531d0 --- /dev/null +++ b/tests/unit/test_xmod_fielddrop.jam @@ -0,0 +1,18 @@ +// An imported struct with a drop-bearing FIELD (and no own cfn drop) +// must still fire the field's drop at scope exit in the entry module. +// Regression: typeNeedsDrop and the field-drop walker once looked the +// struct up by its BARE name against the qualified-keyed registry, +// classified `Outer` as plain data, and silently leaked the field. + +const { assert } = import("test"); +const { Outer, makeOuter } = import("mod_xmod_fielddrop"); + +fn build(s: *mut u32) { + var o: Outer = makeOuter(s); +} + +tfn importedFieldDropFires() { + var hits: u32 = 0; + build(&hits); + assert(hits, 1); +} diff --git a/tests/unit/test_xmod_generics.jam b/tests/unit/test_xmod_generics.jam new file mode 100644 index 0000000..c6110d7 --- /dev/null +++ b/tests/unit/test_xmod_generics.jam @@ -0,0 +1,23 @@ +// Same-named generic factories in two modules. Generic callees once +// resolved by BARE name: `Pair(u64)` interned to one GenericCall +// TypeIdx everywhere, the resolution cache handed whichever module +// resolved first to ALL modules, and the instantiated name `Pair__u64` +// memoized across owners — one module silently used the other's +// methods (or failed on the other's fields). Callees now qualify to +// their owner (`mod_gen_a.Pair`) like every other type. + +const { assert } = import("test"); +const a = import("mod_gen_a"); +const b = import("mod_gen_b"); + +tfn sameNamedGenericsStayDistinct() { + assert(a.makeA(), 18); + assert(b.makeB(), 1009); +} + +tfn entryInstantiatesBothViaHandles() { + var pa: a.Pair(u64) = a.Pair(u64) { x: 5 }; + var pb: b.Pair(u64) = b.Pair(u64) { y: 5 }; + assert(pa.val(), 10); + assert(pb.val(), 1005); +} diff --git a/tests/unit/test_xmod_moves.jam b/tests/unit/test_xmod_moves.jam new file mode 100644 index 0000000..1823828 --- /dev/null +++ b/tests/unit/test_xmod_moves.jam @@ -0,0 +1,36 @@ +// Move tracking for imported drop-bearing types used in the entry +// module. Regression: the analyzer's move gates and method-mode lookup +// once used BARE names against qualified-keyed registries — a bare +// store of an imported drop-bearing value wasn't tracked as a move and +// the value dropped TWICE (double-free), and a move-mode method arg +// went unseen with the same result. Each scenario must drop exactly +// once. + +const { assert } = import("test"); +const { Token, makeToken, Eater, makeEater } = import("mod_xmod_moves"); + +fn moveOnce(s: *mut u32) { + var t: Token = makeToken(s); + // Bare drop-bearing var-init MOVES `t`; only `a` drops at exit. + var a = t; +} + +tfn bareMoveOfImportedDropBearingDropsOnce() { + var hits: u32 = 0; + moveOnce(&hits); + assert(hits, 1); +} + +fn methodMove(s: *mut u32) { + var t: Token = makeToken(s); + var e: Eater = makeEater(); + // move-mode arg: the callee owns and drops `t`; the caller's + // scope-exit drop is suppressed. + e.eat(t); +} + +tfn moveModeMethodArgDropsOnce() { + var hits: u32 = 0; + methodMove(&hits); + assert(hits, 1); +}