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);
+}