diff --git a/src/ast.h b/src/ast.h index a7bbfd2..c907803 100644 --- a/src/ast.h +++ b/src/ast.h @@ -58,14 +58,19 @@ class FunctionAST { bool isPub; bool isTest; bool isVarArgs; + // Declared with `cfn` instead of `fn` — opts the method into + // the compiler-synthesized-call set (drop / at / default). A + // regular `fn` shaped like one of those names is just a method; + // `cfn` is what wires it to the compiler's hooks. + bool isCfn; FunctionAST(std::string Name, std::vector Args, TypeIdx ReturnType, std::vector Body, bool isExtern = false, bool isExport = false, bool isPub = false, bool isTest = false, - bool isVarArgs = false) + bool isVarArgs = false, bool isCfn = false) : Name(std::move(Name)), Args(std::move(Args)), ReturnType(ReturnType), Body(std::move(Body)), isExtern(isExtern), isExport(isExport), - isPub(isPub), isTest(isTest), isVarArgs(isVarArgs) {} + isPub(isPub), isTest(isTest), isVarArgs(isVarArgs), isCfn(isCfn) {} // a function is generic iff any of its parameters has // type `type` (the meta-type) or its return type is `type`. Generic diff --git a/src/astgen.cpp b/src/astgen.cpp index aa2b8e3..516f8e3 100644 --- a/src/astgen.cpp +++ b/src/astgen.cpp @@ -296,6 +296,14 @@ static void emitCondBr(AstGenCtx &gctx, JirRef cond, JirBlockRef thenB, static void emitDrops(AstGenCtx &gctx, const std::vector &bindings); static JirRef emitCall(AstGenCtx &gctx, const FunctionAST *fn, const std::vector &argRefs); +// `v[i]` desugar dispatch — see `emitStructCfnDispatch` for the body. +// Forward-declared so astgenAssign (in this file, above the +// definition) can call it for `v[i] = x` → setAt routing. +static JirRef emitStructCfnDispatch(AstGenCtx &gctx, + const JamCodegenContext::StructInfo *sinfo, + const char *methodName, JirRef recv, + JirRef idx, + const std::vector &extraArgs); // Push an empty drop scope (called when entering a structured body). // We push a parallel `localScopes` frame so each lexical block has @@ -806,6 +814,11 @@ static JirRef astgenLvalue(AstGenCtx &gctx, NodeIdx node, TypeIdx &outLeafTy) { NodeIdx idxIdx = static_cast(n.rhs); JirRef idxRef = astgenExpr(gctx, idxIdx, BuiltinType::U64); const TypeKey &k = gctx.ctx.getTypePool().get(baseTy); + + // Struct indexing in lvalue position is handled exclusively + // via the assignment path (astgenAssign dispatches to + // `cfn setAt`). Nothing to do here — fall through to the + // built-in array / slice / ptr-many index-address logic. TypeIdx elemTy = kNoType; if (k.kind == TypeKind::Array || k.kind == TypeKind::Slice || k.kind == TypeKind::PtrMany) { @@ -856,12 +869,86 @@ static JirRef astgenLvalue(AstGenCtx &gctx, NodeIdx node, TypeIdx &outLeafTy) { } } -// AstGen for `Assign`. Targets: Variable / Deref via lvalue helper. -// Member/Index lvalue writes still need pointer-producing JIR ops — -// added in a follow-up alongside slice/struct-field GEP instructions. +// AstGen for `Assign`. Two paths: +// 1. Target is an Index on a struct value (e.g. `v[i] = x` where +// `v: Vec(i32)`). Dispatch to the struct's `cfn setAt` method — +// that's how value-shaped indexed assignment works without +// producing a pointer / borrow. No `astgenLvalue` step needed +// because there's no pointer-producing JIR op involved; the +// setter takes (recv, i, value) by value and stores internally +// via plain slice-indexing through its own `self.ptr`. +// 2. Anything else — the original lvalue-pointer-then-Store path. +// Variable / Deref / MemberAccess / Index on arrays-slices- +// ptr-many all go through here. static void astgenAssign(AstGenCtx &gctx, const AstNode &n) { NodeIdx targetIdx = static_cast(n.lhs); NodeIdx valueIdx = static_cast(n.rhs); + + // `v[i] = x` on a struct → `v.setAt(i, x)`. + const AstNode &target = gctx.ctx.getNodeStore().get(targetIdx); + if (target.tag == AstTag::Index) { + NodeIdx baseIdx = static_cast(target.lhs); + NodeIdx idxIdx = static_cast(target.rhs); + // Peek the base's type by lowering it as an rvalue. For a + // struct with a `cfn setAt`, the receiver-prep below will + // re-lower as lvalue (mut/move self) or spill (non- + // addressable rvalue) — same shape as the indirect-call + // path. For non-struct bases (arrays / slices / ptr-many), + // we fall back to the existing lvalue-pointer-store path + // without having done extra work the next call can't + // observe (the rvalue lowering is side-effect-free for + // Variable / Index / MemberAccess / Deref bases). + JirRef baseRef = astgenExpr(gctx, baseIdx, kNoType); + TypeIdx baseTy = gctx.jfn.getInst(baseRef).ty; + const auto *sinfo = gctx.ctx.lookupStruct(baseTy); + if (sinfo != nullptr) { + const std::string qualified = sinfo->name + ".setAt"; + const FunctionAST *method = gctx.ctx.getFunctionAST(qualified); + if (method != nullptr && method->isCfn && + method->Args.size() >= 3) { + JirRef idxRef = astgenExpr(gctx, idxIdx, BuiltinType::U64); + // setAt's value parameter type tells us what to + // lower the RHS as. + TypeIdx valParamTy = method->Args[2].Type; + JirRef valRef = astgenExpr(gctx, valueIdx, valParamTy); + // Receiver-prep: setAt's self is mut/move, so we + // hand it a *Self pointer. + ParamMode mode = method->Args[0].Mode; + JirRef recv = baseRef; + if (mode == ParamMode::Mut || mode == ParamMode::Move) { + const AstNode &baseNode = + gctx.ctx.getNodeStore().get(baseIdx); + TypeIdx leafTyR = kNoType; + switch (baseNode.tag) { + case AstTag::Variable: + case AstTag::MemberAccess: + case AstTag::Index: + case AstTag::Deref: + recv = astgenLvalue(gctx, baseIdx, leafTyR); + break; + default: { + JirInst alloca{}; + alloca.tag = JirTag::Alloca; + alloca.ty = baseTy; + JirRef slot = emitAllocaHoisted(gctx, alloca); + JirInst store{}; + store.tag = JirTag::Store; + store.a = slot; + store.b = baseRef; + emit(gctx, store); + recv = slot; + break; + } + } + } + emitStructCfnDispatch(gctx, sinfo, "setAt", recv, idxRef, + {valRef}); + return; + } + } + // Not a struct, or no `cfn setAt` defined — fall through. + } + TypeIdx leafTy = kNoType; JirRef ptrRef = astgenLvalue(gctx, targetIdx, leafTy); JirRef valRef = astgenExpr(gctx, valueIdx, leafTy); @@ -1218,9 +1305,62 @@ static JirRef astgenArrayRepeat(AstGenCtx &gctx, const AstNode &n, return emit(gctx, inst); } +// Look up and call a struct's `cfn` method by name. Returns the +// call's result JirRef (whatever type the method returns), or +// kNoJirRef when the struct doesn't define a matching cfn method. +// Used by the `v[i]` desugar to dispatch to `at` (rvalue read) and +// the `v[i] = x` desugar to dispatch to `setAt` (lvalue write). +// +// Both methods are value-shaped: `at(self, i) T` returns the +// element by value (no pointer); `setAt(self: mut Self, i, value)` +// performs the write. No pointer / address appears in any signature +// — the language's borrow-free MVS model stays intact. +// +// `recv` must be a `*Self` pointer for mut/move self, or the Self +// value for let/const self. Caller decides whether to re-lower or +// spill based on the base AST shape. `extraArgs` holds any args +// beyond (recv, idx) — empty for `at`, single-element [value] for +// `setAt`. +static JirRef emitStructCfnDispatch(AstGenCtx &gctx, + const JamCodegenContext::StructInfo *sinfo, + const char *methodName, JirRef recv, + JirRef idx, + const std::vector &extraArgs) { + std::string qualified = std::string(sinfo->name) + "." + methodName; + const FunctionAST *method = gctx.ctx.getFunctionAST(qualified); + // Method must be declared `cfn` to opt into the compiler's + // index-syntax dispatch. A plain `fn at` / `fn setAt` is just + // an ordinary instance method, called explicitly by the user. + if (method == nullptr || !method->isCfn || + method->Args.size() < 2 + extraArgs.size()) { + return kNoJirRef; + } + // Narrow the U64-typed index to the method's declared index + // parameter width (conventionally u32). + TypeIdx idxParamTy = method->Args[1].Type; + const TypeKey &idxKey = gctx.ctx.getTypePool().get(idxParamTy); + TypeIdx currentTy = gctx.jfn.getInst(idx).ty; + if (currentTy != idxParamTy && idxKey.kind == TypeKind::Int) { + const TypeKey &curKey = gctx.ctx.getTypePool().get(currentTy); + JirInst conv{}; + conv.tag = (idxKey.a < curKey.a) ? JirTag::Trunc : JirTag::ZExt; + conv.a = idx; + conv.ty = idxParamTy; + idx = emit(gctx, conv); + } + std::vector argRefs; + argRefs.reserve(2 + extraArgs.size()); + argRefs.push_back(recv); + argRefs.push_back(idx); + for (JirRef r : extraArgs) argRefs.push_back(r); + return emitCall(gctx, method, argRefs); +} + // AstGen for `Index`. Lowers to JirTag::Index whose codegen handles // the GEP+Load (for stored Variables/Arrays) or alloca-spill (for // SSA aggregates) shape. Element type comes from the base's TypeKey. +// Struct receivers route through `at(self, i)` — see +// `emitStructIndexAtCall`. static JirRef astgenIndex(AstGenCtx &gctx, const AstNode &n) { NodeIdx baseIdx = static_cast(n.lhs); NodeIdx idxIdx = static_cast(n.rhs); @@ -1228,6 +1368,63 @@ static JirRef astgenIndex(AstGenCtx &gctx, const AstNode &n) { JirRef idxRef = astgenExpr(gctx, idxIdx, BuiltinType::U64); TypeIdx baseTy = gctx.jfn.getInst(baseRef).ty; const TypeKey &k = gctx.ctx.getTypePool().get(baseTy); + + // Struct dispatch: `v[i]` → `v.at(i)`. The `at` method is value- + // shaped — it returns T directly, no pointer wrapper. The call's + // result IS the index expression's value; nothing more to do. + // `lookupStruct` chases Struct / Named / GenericCall TypeKinds + // (an un-aliased `Vec(u8)` use-site annotation lands as the + // latter) and returns null for non-struct bases, so we fall + // through cleanly to arrays / slices / ptr-many below. + { + const auto *sinfo = gctx.ctx.lookupStruct(baseTy); + if (sinfo != nullptr) { + const std::string qualified = sinfo->name + ".at"; + const FunctionAST *method = gctx.ctx.getFunctionAST(qualified); + if (method != nullptr && method->isCfn && !method->Args.empty()) { + // Receiver-prep mirrors the indirect-call path: for + // mut/move self, hand the method an addressable + // `*Self`. Re-lower addressable bases via + // astgenLvalue; spill non-addressable rvalues to a + // fresh alloca and use that. (`at` is typically + // `self: Self`, but Vec.at could legitimately take + // `mut self` if the type wants to lazily mutate on + // read — we support both.) + ParamMode mode = method->Args[0].Mode; + JirRef recv = baseRef; + if (mode == ParamMode::Mut || mode == ParamMode::Move) { + const AstNode &baseNode = + gctx.ctx.getNodeStore().get(baseIdx); + TypeIdx leafTy = kNoType; + switch (baseNode.tag) { + case AstTag::Variable: + case AstTag::MemberAccess: + case AstTag::Index: + case AstTag::Deref: + recv = astgenLvalue(gctx, baseIdx, leafTy); + break; + default: { + JirInst alloca{}; + alloca.tag = JirTag::Alloca; + alloca.ty = baseTy; + JirRef slot = emitAllocaHoisted(gctx, alloca); + JirInst store{}; + store.tag = JirTag::Store; + store.a = slot; + store.b = baseRef; + emit(gctx, store); + recv = slot; + break; + } + } + } + JirRef atResult = + emitStructCfnDispatch(gctx, sinfo, "at", recv, idxRef, {}); + if (atResult != kNoJirRef) { return atResult; } + } + } + } + TypeIdx elemTy = kNoType; if (k.kind == TypeKind::Array || k.kind == TypeKind::Slice || k.kind == TypeKind::PtrMany) { diff --git a/src/codegen.cpp b/src/codegen.cpp index 10b4511..ef90166 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -1014,7 +1014,8 @@ TypeIdx JamCodegenContext::instantiateStructExpr( auto cloned = std::make_unique( instMethodName, std::move(instArgs), instReturn, origMethod->Body, origMethod->isExtern, origMethod->isExport, - origMethod->isPub, origMethod->isTest, origMethod->isVarArgs); + origMethod->isPub, origMethod->isTest, origMethod->isVarArgs, + origMethod->isCfn); FunctionAST *clonePtr = cloned.get(); instantiatedMethods_.push_back(std::move(cloned)); diff --git a/src/drop_registry.cpp b/src/drop_registry.cpp index 043e99c..9c5923a 100644 --- a/src/drop_registry.cpp +++ b/src/drop_registry.cpp @@ -13,12 +13,18 @@ namespace jam { namespace drops { // Inspect a candidate function and, if it has the drop-fn shape -// (`fn drop(self: mut )`), add it to the registry under the -// struct's name. Used by both the top-level-function scan and the -// struct-method scan below. +// (`cfn drop(self: mut )`), add it to the registry under +// the struct's name. Used by both the top-level-function scan and +// the struct-method scan below. +// +// `cfn` (not plain `fn`) is required: only methods opted into the +// compiler-synthesized-call set get auto-fired at scope exit. A +// plain `fn drop(self)` is treated as an ordinary method the user +// invokes explicitly — no implicit destructor call. static void considerDropCandidate(const FunctionAST *fn, const TypePool &types, const StringPool &strings, DropRegistry ®istry) { + if (!fn->isCfn) return; if (fn->Name != "drop") return; if (fn->Args.size() != 1) return; const Param &p = fn->Args[0]; diff --git a/src/lexer.cpp b/src/lexer.cpp index e08e068..2f8f159 100644 --- a/src/lexer.cpp +++ b/src/lexer.cpp @@ -98,6 +98,8 @@ void Lexer::identifier() { // Check for keywords if (text == "fn") { addToken(TOK_FN); + } else if (text == "cfn") { + addToken(TOK_CFN); } else if (text == "return") { addToken(TOK_RETURN); } else if (text == "const") { diff --git a/src/main.cpp b/src/main.cpp index 466041f..6a49da5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -536,53 +536,41 @@ static int compileAndRun(const std::string &filename, }; for (auto &s : module->Structs) { for (auto &m : s->Methods) { - // Two privileged method names on top-level structs: - // `drop` — no-arg-cleanup; must take `self: mut Self`. - // `default` — opt-in default constructor; takes no - // parameters, returns `Self`. Used by anything - // that expects a default value (struct-literal - // omitted fields, generic type parameters that - // require a default, future `var x: T;`). - // Anything else is rejected — there is no general user- - // defined static-method or instance-method support on top- - // level structs in v1. - if (m->Name == "default") { + // `cfn`-marked methods (drop / default / at / …) opt in + // to compiler-synthesized calls and must match the + // expected signature for their name. Plain `fn` methods + // are ordinary instance methods — no signature + // constraints, no rejection by name. + if (m->isCfn && m->Name == "default") { if (!m->Args.empty()) { std::cerr << filename - << ": error: method `default` on struct `" - << s->Name << "` must take no parameters\n"; + << ": error: cfn `default` on struct `" << s->Name + << "` must take no parameters\n"; return 1; } std::string retStruct = resolveStructName(m->ReturnType); if (retStruct != s->Name) { - std::cerr - << filename << ": error: method `default` on struct `" - << s->Name << "` must return `Self` (got `" << retStruct - << "`)\n"; + std::cerr << filename + << ": error: cfn `default` on struct `" << s->Name + << "` must return `Self` (got `" << retStruct + << "`)\n"; return 1; } - } else if (m->Name == "drop") { + } else if (m->isCfn && m->Name == "drop") { if (m->Args.empty() || m->Args[0].Name != "self") { - std::cerr << filename << ": error: method `" << m->Name + std::cerr << filename << ": error: cfn `" << m->Name << "` on struct `" << s->Name << "` must take `self` as its first parameter\n"; return 1; } std::string selfStruct = resolveStructName(m->Args[0].Type); if (selfStruct != s->Name) { - std::cerr << filename << ": error: method `" << m->Name + std::cerr << filename << ": error: cfn `" << m->Name << "` on struct `" << s->Name << "` has self type `" << selfStruct << "`; expected `" << s->Name << "`\n"; return 1; } - } else { - std::cerr << filename - << ": error: only `drop` and `default` " - "methods are allowed on top-level " - "structs (saw `" - << s->Name << "." << m->Name << "`)\n"; - return 1; } { JirFunction jfn = astgenMetadata(*m, codegenCtx); diff --git a/src/parser.cpp b/src/parser.cpp index a563401..8f42d5f 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -150,8 +150,8 @@ bool Parser::isQualifiedNameChain(NodeIdx chainRoot) const { // Walk the chain root (leftmost Variable in a Variable.member.member... // chain) and return its source-level name. Caller guarantees the chain // is a qualified-name shape. -static std::string chainRootName(const NodeStore &ns, - const StringPool &pool, NodeIdx chainRoot) { +static std::string chainRootName(const NodeStore &ns, const StringPool &pool, + NodeIdx chainRoot) { const AstNode &n = ns.get(chainRoot); if (n.tag == AstTag::Variable) { return pool.get(static_cast(n.lhs)); @@ -320,8 +320,8 @@ NodeIdx Parser::parsePrimary() { typeNameId = stringPool->intern(structContextStack.back()); } AstNode &litNode = nodes->getMut(lit); - litNode.lhs = static_cast( - typePool->internNamed(typeNameId)); + litNode.lhs = + static_cast(typePool->internNamed(typeNameId)); return lit; } @@ -395,8 +395,7 @@ NodeIdx Parser::parsePrimary() { std::string receiverName = isNamespacedTypecall ? qualifiedName(expr) : name; TypeIdx genericTy = typePool->internGenericCall( - stringPool->intern(receiverName), - std::move(typeArgs)); + stringPool->intern(receiverName), std::move(typeArgs)); NodeIdx lit = parseStructLiteral(); AstNode &litNode = nodes->getMut(lit); litNode.lhs = static_cast(genericTy); @@ -513,7 +512,8 @@ NodeIdx Parser::parsePrimary() { callee = name; } StringIdx calleeId = stringPool->intern(callee); - expr = emit(AstNode{AstTag::Call, 0, 0, 0, calleeId, extra}); + expr = + emit(AstNode{AstTag::Call, 0, 0, 0, calleeId, extra}); } } else { expr = emit(AstNode{AstTag::Call, 0, 1, 0, @@ -1292,7 +1292,18 @@ std::unique_ptr Parser::parseFunction() { else break; } - if (!isTest) { consume(TOK_FN, "Expected 'fn' keyword"); } + // Accept `cfn` as a marker for compiler-synthesized-call methods + // (drop / at / default). Same shape as `fn` otherwise — modifiers + // loop above is unchanged, the body is identical, only the flag + // on FunctionAST differs. + bool isCfn = false; + if (!isTest) { + if (match(TOK_CFN)) { + isCfn = true; + } else { + consume(TOK_FN, "Expected 'fn' or 'cfn' keyword"); + } + } consume(TOK_IDENTIFIER, "Expected function name"); std::string name(previous().text(source_)); @@ -1344,9 +1355,9 @@ std::unique_ptr Parser::parseFunction() { if (isExtern) { consume(TOK_SEMI, "Expected ';' after extern function declaration"); - return std::make_unique(name, std::move(args), returnType, - std::vector{}, true, - isExport, isPub, false, isVarArgs); + return std::make_unique( + name, std::move(args), returnType, std::vector{}, true, + isExport, isPub, false, isVarArgs, isCfn); } consume(TOK_OPEN_BRACE, "Expected '{' before function body"); @@ -1359,17 +1370,18 @@ std::unique_ptr Parser::parseFunction() { return std::make_unique(name, std::move(args), returnType, std::move(body), false, isExport, - isPub, isTest, false); + isPub, isTest, false, isCfn); } void Parser::parseStructBody( std::vector> &fields, std::vector> &methods) { while (!check(TOK_CLOSE_BRACE) && !isAtEnd()) { - // Method: `fn name(self: ..., ...) ReturnType { body }`. Methods - // can appear in any order relative to fields. parseFunction - // consumes the `fn` keyword itself. - if (check(TOK_FN)) { + // Method: `fn name(self: ..., ...) ReturnType { body }` (or + // `cfn name(...)` for compiler-synthesized-call methods). + // Methods can appear in any order relative to fields. + // parseFunction consumes the keyword itself. + if (check(TOK_FN) || check(TOK_CFN)) { methods.push_back(parseFunction()); match(TOK_COMMA); // optional trailing comma after a method continue; diff --git a/src/token.h b/src/token.h index 9264b5b..7ad64f1 100644 --- a/src/token.h +++ b/src/token.h @@ -16,6 +16,12 @@ enum TokenType { TOK_EOF = 0, TOK_FN, + // `cfn` declares a method whose calls are synthesized by the + // compiler instead of being typed by the user. The shape on disk + // is identical to a regular function (same args / body / return); + // the difference is opt-in for the compiler hooks (`drop`, `at`, + // `default`). A `fn drop(self)` without `cfn` is just a method. + TOK_CFN, TOK_IDENTIFIER, TOK_COLON, TOK_OPEN_BRACE, diff --git a/std/collections.jam b/std/collections.jam index c8cedc6..4d4d38c 100644 --- a/std/collections.jam +++ b/std/collections.jam @@ -74,6 +74,22 @@ pub fn Vec(T: type) type { return Option(T).Some(self.ptr[i]); } + // `cfn at` / `cfn setAt` are the value-shaped hooks the + // compiler dispatches `v[i]` (rvalue) and `v[i] = x` + // (lvalue) to. Both signatures are pure value semantics — + // no `*mut T` return, no `&` in any body, no address ever + // produced. The implementation uses slice-indexing through + // the private `self.ptr` field, which is a structured + // primitive op (not pointer arithmetic). + // + // Bounds-checked reads remain on `get(i) -> Option(T)`. + cfn at(self: Self, i: u32) T { + return self.ptr[i]; + } + cfn setAt(self: mut Self, i: u32, value: T) { + self.ptr[i] = value; + } + // Reset length to zero without releasing the buffer. Lets // callers reuse one Vec across many iterations of a hot // loop (per-frame audio drain, per-sector decode scratch) @@ -154,7 +170,10 @@ pub fn Vec(T: type) type { self.capacity = newCap; } - fn drop(self: mut Self) { + // `cfn drop` is what hooks Vec into MVS auto-cleanup. A + // plain `fn drop` would not auto-fire at scope exit; only + // the cfn variant is registered with the drop registry. + cfn drop(self: mut Self) { free(self.ptr as *mut[] u8); } }; diff --git a/tests/cpp/test_diagnostics.cpp b/tests/cpp/test_diagnostics.cpp index 6493631..f75e6b1 100644 --- a/tests/cpp/test_diagnostics.cpp +++ b/tests/cpp/test_diagnostics.cpp @@ -160,19 +160,19 @@ void testMultiErrorsSortedByLine() { // ── Reference trace for generic instantiation ────────────────── void testGenericInstantiationCarriesRefTrace() { - auto r = - compileSource("diag_ref_trace", "fn Box(T: type) type {\n" - " return struct {\n" - " val: T,\n" - " fn pickBad(self: Self) i32 {\n" - " return self.notAField;\n" - " }\n" - " };\n" - "}\n" - "fn main() i32 {\n" - " var b: Box(i32) = Box(i32) { val: 7 };\n" - " return b.pickBad();\n" - "}\n"); + auto r = compileSource("diag_ref_trace", + "fn Box(T: type) type {\n" + " return struct {\n" + " val: T,\n" + " fn pickBad(self: Self) i32 {\n" + " return self.notAField;\n" + " }\n" + " };\n" + "}\n" + "fn main() i32 {\n" + " var b: Box(i32) = Box(i32) { val: 7 };\n" + " return b.pickBad();\n" + "}\n"); ASSERT_TRUE(r.exitCode != 0); // Underlying error inside the instantiated body — line 5 in // source. Reference trace adds "in instantiation of diff --git a/tests/unit/test_cfn.jam b/tests/unit/test_cfn.jam new file mode 100644 index 0000000..370ac59 --- /dev/null +++ b/tests/unit/test_cfn.jam @@ -0,0 +1,65 @@ +// `cfn` marker — opts a method into the compiler-synthesized-call +// set (drop, at, default). A plain `fn` with one of those names is +// just an ordinary method: no auto-fire at scope exit for `drop`, +// no `v[i]` desugar for `at`. These tests pin that behavior. + +const { assert } = import("test"); + +// cfn drop fires on scope exit + +const CfnDropTarget = struct { + sink: *mut u32, + cfn drop(self: mut CfnDropTarget) { + var p: *mut u32 = self.sink; + p.* = p.* + 1; + } +}; + +fn makeAndDropCfn(sink: *mut u32) { + var d: CfnDropTarget = CfnDropTarget { sink: sink }; + // d falls out of scope here — cfn drop must run. +} + +tfn cfnDropAutoFires() { + var counter: u32 = 0; + var p: *mut u32 = &counter; + makeAndDropCfn(p); + assert(counter, 1); +} + +// fn drop (no cfn) does NOT auto-fire + +const PlainDropTarget = struct { + sink: *mut u32, + // Same shape as a drop method but declared `fn`, not `cfn`. The + // compiler must treat this as an ordinary instance method — + // never synthesize a call at scope exit. + fn drop(self: mut PlainDropTarget) { + var p: *mut u32 = self.sink; + p.* = p.* + 1; + } +}; + +fn makeAndDropPlain(sink: *mut u32) { + var d: PlainDropTarget = PlainDropTarget { sink: sink }; + // Falls out of scope — no auto-call. Caller still observes 0. +} + +tfn plainDropDoesNotAutoFire() { + var counter: u32 = 0; + var p: *mut u32 = &counter; + makeAndDropPlain(p); + // If the compiler synthesized a call to PlainDropTarget.drop + // here, counter would be 1. + assert(counter, 0); +} + +// Explicit call to a plain `fn drop` still works as a regular +// instance method — the name isn't reserved, just the marker. +tfn plainDropCallableExplicitly() { + var counter: u32 = 0; + var p: *mut u32 = &counter; + var d: PlainDropTarget = PlainDropTarget { sink: p }; + d.drop(); + assert(counter, 1); +} diff --git a/tests/unit/test_drops.jam b/tests/unit/test_drops.jam index ca33d90..f9d915f 100644 --- a/tests/unit/test_drops.jam +++ b/tests/unit/test_drops.jam @@ -14,7 +14,7 @@ const Counter = struct { // `var c: Counter = ...;` goes out of scope, the codegen synthesizes a // call to drop(&c). We bump the sink-pointed value so we can observe it // from the test harness. -fn drop(self: mut Counter) { +cfn drop(self: mut Counter) { // Pointer-deref assignment is only supported on a pointer-typed local // today, so move the field into a temp before reading/writing. var p: *mut u32 = self.sink; diff --git a/tests/unit/test_drops_loops.jam b/tests/unit/test_drops_loops.jam index de7329a..b8579de 100644 --- a/tests/unit/test_drops_loops.jam +++ b/tests/unit/test_drops_loops.jam @@ -8,7 +8,7 @@ const Bumper = struct { sink: *mut u32, }; -fn drop(self: mut Bumper) { +cfn drop(self: mut Bumper) { var p: *mut u32 = self.sink; p.* = p.* + 1; } diff --git a/tests/unit/test_drops_mangling.jam b/tests/unit/test_drops_mangling.jam index 838d160..a18b5b0 100644 --- a/tests/unit/test_drops_mangling.jam +++ b/tests/unit/test_drops_mangling.jam @@ -1,6 +1,6 @@ const { assert } = import("test"); -// MVS P8.2a: two `fn drop(self: mut T)` for different types coexist +// Two `fn drop(self: mut T)` for different types coexist // because the codegen mangles each to `__drop_` at the LLVM // level. Each fires for its own type at scope exit. @@ -12,12 +12,12 @@ const B = struct { sink: *mut u32, }; -fn drop(self: mut A) { +cfn drop(self: mut A) { var p: *mut u32 = self.sink; p.* = p.* + 10; } -fn drop(self: mut B) { +cfn drop(self: mut B) { var p: *mut u32 = self.sink; p.* = p.* + 100; } diff --git a/tests/unit/test_drops_scoped.jam b/tests/unit/test_drops_scoped.jam index 5fa8998..e9e03a8 100644 --- a/tests/unit/test_drops_scoped.jam +++ b/tests/unit/test_drops_scoped.jam @@ -8,7 +8,7 @@ const Bumper = struct { sink: *mut u32, }; -fn drop(self: mut Bumper) { +cfn drop(self: mut Bumper) { var p: *mut u32 = self.sink; p.* = p.* + 1; } diff --git a/tests/unit/test_struct_methods.jam b/tests/unit/test_struct_methods.jam index ac44f37..195c458 100644 --- a/tests/unit/test_struct_methods.jam +++ b/tests/unit/test_struct_methods.jam @@ -8,7 +8,7 @@ const { assert } = import("test"); const Counter = struct { value: u32, sink: *mut u32, - fn drop(self: mut Counter) { + cfn drop(self: mut Counter) { var p: *mut u32 = self.sink; p.* = p.* + 1; } diff --git a/tests/unit/test_vec.jam b/tests/unit/test_vec.jam index 311b93c..a753482 100644 --- a/tests/unit/test_vec.jam +++ b/tests/unit/test_vec.jam @@ -248,3 +248,119 @@ tfn appendSliceAfterExistingData() { assert(unwrapU8(v.get(0), 0) as i32, 65); assert(unwrapU8(v.get(3), 0) as i32, 68); } + +// `v[i] = x` and `x = v[i]` route through the compiler's special +// `at(self, i) *mut T` dispatch — no `.ptr` reach-through in user +// code. The semantics match raw `*mut[] T` indexing (no bounds +// check); bounds-checked reads remain available via `get(i)`. + +tfn indexedWriteAndRead() { + var v: VecI32 = VecI32.withCapacity(4); + v[0] = 10; + v[1] = 20; + v[2] = 30; + v[3] = 40; + // Direct reads through the same `at` path. + assert(v[0], 10); + assert(v[1], 20); + assert(v[2], 30); + assert(v[3], 40); +} + +tfn indexedReadAfterPush() { + var v: VecI32 = VecI32.empty(); + v.push(7); + v.push(8); + v.push(9); + // Pushed values are visible through indexed reads (the buffer + // is the same — push just bumps length). + assert(v[0], 7); + assert(v[1], 8); + assert(v[2], 9); +} + +tfn indexedWriteVisibleThroughGet() { + var v: VecI32 = VecI32.empty(); + v.push(1); + v.push(2); + v.push(3); + v[1] = 99; + // `get(i)` reads through the same backing store, so the + // indexed write is visible — confirms both sides agree on the + // pointer-math vs the Vec's element view. + assert(unwrapI32(v.get(1), 0), 99); +} + +tfn indexedU8Vec() { + // VecU8 — different T, exercises generic instantiation of + // `at` plus the compiler's u64→u32 narrowing on the index. + var v: VecU8 = VecU8.withCapacity(3); + v[0] = 100; + v[1] = 200; + v[2] = 250; + assert(v[0] as i32, 100); + assert(v[1] as i32, 200); + assert(v[2] as i32, 250); +} + +tfn indexedCompoundExpression() { + // The index can be an arbitrary u32-typed expression, not + // just a literal. Verifies the index parameter is lowered + // through the normal expression path before `at` is called. + var v: VecI32 = VecI32.withCapacity(8); + var i: u32 = 0; + while (i < 8) { + v[i] = (i as i32) * 10; + i = i + 1; + } + assert(v[0], 0); + assert(v[3], 30); + assert(v[7], 70); +} + +// Inline `Vec(T)` (no alias) goes through the same `at` dispatch. +// The use-site annotation produces a TypeKind::GenericCall instead +// of TypeKind::Named, so the compiler's struct-dispatch lookup has +// to chase through lookupStruct's GenericCall arm. + +tfn inlineVecTypeAnnotation() { + var v: Vec(i32) = Vec(i32).withCapacity(4); + v[0] = 11; + v[1] = 22; + v[2] = 33; + v[3] = 44; + assert(v[0], 11); + assert(v[3], 44); +} + +tfn inlineVecU8WithCapacity() { + var v: Vec(u8) = Vec(u8).withCapacity(3); + v[0] = 1; + v[1] = 2; + v[2] = 3; + assert(v[0] as i32, 1); + assert(v[2] as i32, 3); +} + +tfn inlineVecEmpty() { + // Empty constructor exercises the same instantiation path — + // confirms `at` is wired up regardless of which factory the + // Vec came from. + var v: Vec(i32) = Vec(i32).empty(); + v.push(7); + v.push(8); + assert(v[0], 7); + assert(v[1], 8); + v[0] = 70; + assert(v[0], 70); +} + +tfn inlineVecMethodsStillWork() { + // Non-index methods (push, len, get) still resolve when the + // declared type is the inline `Vec(T)` form. + var v: Vec(u8) = Vec(u8).empty(); + v.push(100); + v.push(101); + assert(v.len() as i32, 2); + assert(unwrapU8(v.get(0), 0) as i32, 100); +}