diff --git a/src/astgen.cpp b/src/astgen.cpp index 8335e22..acee9f3 100644 --- a/src/astgen.cpp +++ b/src/astgen.cpp @@ -4517,6 +4517,10 @@ static JirRef astgenCompInstantiatedCall(AstGenCtx &gctx, const AstNode &n, instName, std::move(instArgs), fn->ReturnType, fn->Body, fn->isExtern, fn->isExport, fn->isPub, fn->isTest, fn->isVarArgs, fn->isCfn); + // Note: modulePath stays empty on the clone — stamping it + // would alter the mangled symbol name (mangling.h:60). Body- + // scope identifier resolution is carried separately via + // bodyModuleStack_, pushed below around astgenBodyInto. clone = gctx.ctx.adoptInstantiatedFunction(std::move(cloned)); gctx.ctx.registerFunctionAST(instName, const_cast(clone)); @@ -4529,7 +4533,9 @@ static JirRef astgenCompInstantiatedCall(AstGenCtx &gctx, const AstNode &n, JirFunction jfn = astgenMetadata(*clone, gctx.ctx); jfn.name = clone->Name; jirDeclarePrototype(jfn, gctx.ctx); + gctx.ctx.pushBodyModule(fn->modulePath); astgenBodyInto(jfn, *clone, gctx.ctx); + gctx.ctx.popBodyModule(); jirDefineBody(jfn, gctx.ctx); gctx.ctx.clearCurrentCompSubst(); diff --git a/src/codegen.cpp b/src/codegen.cpp index 8331396..7547a3c 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -465,10 +465,48 @@ void JamCodegenContext::registerFunctionAST(const std::string &name, const FunctionAST * JamCodegenContext::getFunctionAST(const std::string &name) const { + // When lowering the body of a generic instantiated from a different + // module, resolve identifiers against the DEFINING MODULE's + // namespace first — never the caller's. A body like Vec(T).drop + // calling `free` must find `pub extern fn free` in + // std/collections.jam (where Vec lives) regardless of whether the + // caller module independently imports it. + // + // Falls back to the flat global map for two cases the per-module + // table doesn't cover: (a) generic instantiations registered by + // mangled name (`Vec__u32.withCapacity` etc.) that live in the + // global map but not in any source module's pub-fn table; (b) + // names looked up outside any instantiation body (normal entry- + // module function bodies). + if (!bodyModuleStack_.empty()) { + const std::string &defMod = bodyModuleStack_.back(); + if (!defMod.empty()) { + auto nsIt = moduleNamespaces_.find(defMod); + if (nsIt != moduleNamespaces_.end()) { + auto fIt = nsIt->second.functions.find(name); + if (fIt != nsIt->second.functions.end()) { + return fIt->second; + } + } + } + } auto it = functionAsts.find(name); return (it == functionAsts.end()) ? nullptr : it->second; } +void JamCodegenContext::pushBodyModule(const std::string &modulePath) { + bodyModuleStack_.push_back(modulePath); +} + +void JamCodegenContext::popBodyModule() { + if (!bodyModuleStack_.empty()) bodyModuleStack_.pop_back(); +} + +const std::string &JamCodegenContext::currentBodyModule() const { + static const std::string kEmpty; + return bodyModuleStack_.empty() ? kEmpty : bodyModuleStack_.back(); +} + void JamCodegenContext::registerImportHandle(const std::string &handle, const std::string &modulePath) { importHandles_[handle].modulePath = modulePath; @@ -1004,8 +1042,13 @@ TypeIdx JamCodegenContext::resolveGenericCall(TypeIdx callTy) const { 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`. - result = instantiateStructExpr(value, generic->Name, args, subst); + // `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); break; } if (value.tag == AstTag::EnumExpr) { @@ -1036,7 +1079,8 @@ TypeIdx JamCodegenContext::resolveGenericCall(TypeIdx callTy) const { TypeIdx JamCodegenContext::instantiateStructExpr( 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 (!anonStructs_) { throw std::runtime_error( "internal: anonymous struct table not registered on " @@ -1164,6 +1208,11 @@ TypeIdx JamCodegenContext::instantiateStructExpr( origMethod->Body, origMethod->isExtern, origMethod->isExport, origMethod->isPub, origMethod->isTest, origMethod->isVarArgs, origMethod->isCfn); + // Note: modulePath stays empty on the clone — stamping it + // would alter the mangled symbol name (mangling.h:60 + // includes modulePath in the FQN). Body-scope identifier + // resolution is carried separately via bodyModuleStack_, + // pushed at the astgenBodyInto call site below. FunctionAST *clonePtr = cloned.get(); instantiatedMethods_.push_back(std::move(cloned)); @@ -1211,15 +1260,22 @@ TypeIdx JamCodegenContext::instantiateStructExpr( jam::Diagnostic::Trace traceFrame{/*loc=*/{}, /*decl=*/im.clonePtr->Name}; jam::RefTraceFrame guard(refTrace_, std::move(traceFrame)); + // Push the generic's defining module so bare-identifier + // lookups in the body (e.g. `free` in Vec(T).drop) + // resolve against that module's namespace. RAII via + // try/catch: pop on every exit path. + mutCtx.pushBodyModule(definingModulePath_); try { astgenBodyInto(im.passOneJir, *im.clonePtr, mutCtx); } catch (const AstGenAnalysisFail &) { // diagnostic already pushed; trace was attached via // the helper. Continue with the next method so the // user sees every error in this instantiation. + mutCtx.popBodyModule(); clearCurrentSubst(); continue; } + mutCtx.popBodyModule(); auto diags = verifyJirFunction( im.passOneJir, &typePool, &stringPool, +[](void *c, TypeIdx t) -> TypeIdx { diff --git a/src/codegen.h b/src/codegen.h index 567f3d3..b041882 100644 --- a/src/codegen.h +++ b/src/codegen.h @@ -271,6 +271,19 @@ class JamCodegenContext { void registerFunctionAST(const std::string &name, const FunctionAST *fn); const FunctionAST *getFunctionAST(const std::string &name) const; + // Push/pop the module path whose body is currently being lowered. + // A generic instantiated from a different module needs its body's + // bare-identifier lookups to resolve against the generic's + // defining module — e.g. Vec(T).drop calling `free` resolves + // against std/collections.jam (where `pub extern fn free` lives), + // not the caller's scope. `astgenBodyInto` of an instantiated + // generic pushes the originating modulePath before lowering and + // pops after; `getFunctionAST` consults the top-of-stack module's + // namespace first. + void pushBodyModule(const std::string &modulePath); + void popBodyModule(); + const std::string ¤tBodyModule() const; + struct ImportHandleInfo { std::string modulePath; std::unordered_set privateNames; @@ -331,6 +344,11 @@ class JamCodegenContext { std::unordered_map importHandles_; // Resolved canonical path -> namespace decl table. See ModuleNamespace. std::unordered_map moduleNamespaces_; + // Stack of `modulePath` strings for the bodies currently being lowered. + // Top-of-stack is the defining module of the innermost generic body — + // consulted by `getFunctionAST` to resolve identifiers against the + // generic's home module. + std::vector bodyModuleStack_; // `genericResolutions_` memoizes per-callsite: every unique // `TypeKind::GenericCall` TypeIdx maps to the resolved TypeIdx. @@ -489,7 +507,8 @@ class JamCodegenContext { TypeIdx instantiateStructExpr( 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; // Mirror of instantiateStructExpr for `enum { ... }` expressions. // Substitutes each variant's payload types with the concrete diff --git a/src/main.cpp b/src/main.cpp index 2dcbc12..84a795a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -325,22 +325,38 @@ static int compileAndRun(const std::string &filename, // methods. Those methods call externs from the same imported // module (e.g. `malloc`), so the externs need prototypes // declared by the time the instantiation runs. + // + // Split into two passes so iteration order over loaded modules + // doesn't matter: Pass A registers every module's pub fns + + // ModuleNamespace into the global registry; Pass B emits the LLVM + // prototypes. Without the split, if `bus.jam` is iterated before + // `std/collections` (unordered_map order), bus's `dma: Vec(u32)` + // field triggers Vec instantiation whose body calls `malloc` — + // but std/collections's `malloc` hasn't been globally registered + // yet, so the body fails to compile. for (const auto &[path, importedModule] : resolver.getLoadedModules()) { - // Per-loaded-module namespace, keyed by the canonical import - // path (e.g. "fmt", "std/fmt"). This is the table member-access - // on a Module value will consult later. JamCodegenContext::ModuleNamespace ns; ns.path = path; for (auto &func : importedModule->Functions) { - if (func->isPub && !func->isGeneric()) { - JirFunction jfn = astgenMetadata(*func, codegenCtx); - jfn.name = mangledFunctionName(*func, codegenCtx.getTypePool(), - codegenCtx.getStringPool()); - jirDeclarePrototype(jfn, codegenCtx); - } if (func->isPub) { codegenCtx.registerFunctionAST(func->Name, func.get()); ns.functions[func->Name] = func.get(); + // Eagerly declare LLVM prototypes for pub-extern fns + // (libc allocator, write, putchar, …). A generic + // instantiation in Pass B may emit a Call to one of + // these BEFORE the iteration reaches its defining + // module — without an eager declaration, the call + // lowers to "unknown callee `malloc`". Non-extern + // pub fns still need their full signature to be + // resolved against module-level types, so we defer + // those to Pass B. + if (func->isExtern && !func->isGeneric()) { + JirFunction jfn = astgenMetadata(*func, codegenCtx); + jfn.name = mangledFunctionName( + *func, codegenCtx.getTypePool(), + codegenCtx.getStringPool()); + jirDeclarePrototype(jfn, codegenCtx); + } } } for (auto &s : importedModule->Structs) { @@ -361,8 +377,6 @@ static int compileAndRun(const std::string &filename, codegenCtx.getStringPool().intern(u->Name)); } } - // `pub const X = import(...)` re-exports — surface the inner - // module as a Module-typed alias on this module's namespace. for (auto &reexport : importedModule->Imports) { if (!reexport->isPub) continue; if (reexport->Path == "test") continue; @@ -372,6 +386,21 @@ static int compileAndRun(const std::string &filename, } codegenCtx.registerModuleNamespace(std::move(ns)); } + // Pass B: now that every module's pub fns are visible, emit LLVM + // prototypes. Any generic instantiation triggered by a parameter + // or return type resolves its body's externs against the + // already-populated global registry — or, via getFunctionAST's + // fallback, against the generic's defining-module namespace. + for (const auto &[path, importedModule] : resolver.getLoadedModules()) { + for (auto &func : importedModule->Functions) { + if (func->isPub && !func->isGeneric()) { + JirFunction jfn = astgenMetadata(*func, codegenCtx); + jfn.name = mangledFunctionName(*func, codegenCtx.getTypePool(), + codegenCtx.getStringPool()); + jirDeclarePrototype(jfn, codegenCtx); + } + } + } // Register every flat `handle.X` mapping for a given (handle name, // resolved module). Shared by direct-import bindings and module- // valued destructuring bindings (`const {fmt} = import("std");`). diff --git a/std/buf.jam b/std/buf.jam deleted file mode 100644 index 1cf3459..0000000 --- a/std/buf.jam +++ /dev/null @@ -1,106 +0,0 @@ -// std.buf — owning fixed-size heap slice of `T`. -// -// Sibling to `std/box`: where `Box(T)` owns exactly one T, `Buf(T)` owns -// a contiguous run of N Ts. N is set at allocation (runtime-sized), not at -// the type level — making `Buf(T)` the closest jam analog to Rust's -// `Box<[T]>`. The backing allocator is libc malloc/free, same TODO as Vec -// re: pluggable Allocator support once function pointers land. -// -// API: -// Buf(T).filled(v, n) — alloc + per-slot typed write of `v`. The primary -// (and only) constructor — Buf is always initialised. -// Safe for any T; optimizer recovers a memset for -// primitive T at -O1+. -// Buf(T).fromRaw(p,n) — UNSAFE: take ownership of an existing heap -// allocation. For ownership transfers (Vec→Buf, FFI). -// buf.len() — element count (set at construction) -// buf[i] / buf[i] = v — indexed read / write (cfn at / cfn setAt hooks) -// buf.typed() — *mut[] T borrow (pass through to APIs taking that) -// buf.raw() — *mut[] u8 borrow (for FFI that asks for bytes) -// -// Drop semantics: leaving the owning scope calls `cfn drop`, which frees -// the allocation. Move-only — copying would double-free; the drop registry -// enforces this via MVS at the use-site. -// -// TODO: re-introduce typed-uninit construction via `Buf(Maybe(T))`. The -// design landed once (see std/maybe.jam's `Maybe(T).assumeInitBuf`) but -// needs three jam-compiler pieces before it can ship as the public uninit -// path without leaking unsafe escape hatches: -// - a typed-uninit constructor (e.g. `Buf(Maybe(T)).new(n)`) that -// doesn't expose an `allocUninit` for `T` itself; -// - `as` cast support for typed-pointer ↔ typed-pointer (currently must -// bridge via `*mut[] u8`); -// - safer move semantics in `assumeInitBuf` so the disarm-via-null -// dance can drop. Until then, `Buf` is always initialised and the -// uninit-fill case lives in `Vec(T).withCapacity` + `setLen`. - -pub extern fn malloc(size: u64) *mut[] u8; -pub extern fn free(ptr: *mut[] u8); - -pub fn Buf(T: type) type { - return struct { - ptr: *mut[] T, - length: u32, - - // Construct a Buf from raw parts (a typed pointer to a heap - // allocation of `length` Ts that the new Buf takes ownership of - // and frees on drop). UNSAFE: caller must ensure `ptr` came from - // `malloc`-compatible allocation sized for `length` Ts AND that - // no other Buf owns the same pointer (else double-free). Useful - // for ownership transfers (e.g. Vec → Buf conversion, FFI handoffs) - // and the typed-uninit upgrade in std/maybe.jam's `assumeInitBuf`. - pub fn fromRaw(ptr: *mut[] T, length: u32) Self { - return Self { ptr: ptr, length: length }; - } - - // The primary constructor: allocate `n` Ts and fill every slot - // with `v` via typed writes. Safe for any T — no raw-byte - // interpretation; the value `v` is what lands in every slot. For - // primitive T (u8 / u32) the loop collapses to an `llvm.memset` - // at -O1+, so `Buf(u8).filled(0, N)` is the safe stand-in for an - // `extern fn memset` zero-fill. - // - // Buf is **always initialized** — there's no uninit-alloc form. - // For "alloc then fread into bytes" use Vec(T)'s `withCapacity`+ - // `setLen` pattern; convert to Buf via `fromRaw` if needed. - fn filled(v: T, n: u32) Self { - const bytes: u64 = (n as u64) * @sizeOf(T); - var raw: *mut[] u8 = malloc(bytes); - var s: Self = Self { ptr: raw as *mut[] T, length: n }; - var i: u32 = 0; - while (i < n) { - s.ptr[i] = v; - i = i + 1; - } - return s; - } - - pub fn len(self: Self) u32 { return self.length; } - - // Typed pointer borrow — for device APIs that already take a - // `*mut[] T` (`pub fn dmaUpdate(d: *mut[] u32, ...)`). The Buf - // remains the owner; the borrow is valid while the Buf is in scope. - pub fn typed(self: Self) *mut[] T { return self.ptr; } - - // Byte-pointer borrow — for libc fns (memset / memcpy / fwrite) - // and SDL FFI calls that take `*mut[] u8`. - pub fn raw(self: Self) *mut[] u8 { return self.ptr as *mut[] u8; } - - // `buf[i]` (rvalue) and `buf[i] = v` (lvalue) dispatch hooks. Pure - // value semantics — no `&` / no address ever produced — matching - // Vec's at / setAt contract in std/collections. - cfn at(self: Self, i: u32) T { return self.ptr[i]; } - cfn setAt(self: mut Self, i: u32, v: T) { self.ptr[i] = v; } - - // Hooks the buf into MVS auto-cleanup. Same `cfn drop` pattern as - // Box and Vec. The null-pointer guard lets ownership-transfer - // helpers (std/maybe_init.jam's `assumeInitBuf`) disarm a source - // Buf by zeroing `ptr` — drop on the disarmed source then no-ops - // instead of double-freeing. - cfn drop(self: mut Self) { - if ((self.ptr as u64) != 0) { - free(self.ptr as *mut[] u8); - } - } - }; -} diff --git a/std/collections.jam b/std/collections.jam index e42ecd6..2130fad 100644 --- a/std/collections.jam +++ b/std/collections.jam @@ -48,6 +48,23 @@ pub fn Vec(T: type) type { return Self { ptr: raw as *mut[] T, length: 0, capacity: actualCap }; } + // Rust's `vec![v; n]`. Allocates `n` slots, writes `v` to each, + // and sets length to `n`. The right constructor for fixed-size + // buffers whose every slot is meaningful at construction + // (zero-initialized device state, default-filled padding); use + // `withCapacity(n)` instead when the caller intends to fill via + // `push` and start at length=0. + fn filled(value: T, n: u32) Self { + var v: Self = Self.withCapacity(n); + var i: u32 = 0; + while (i < n) { + v.ptr[i] = value; + i = i + 1; + } + v.length = n; + return v; + } + fn push(self: mut Self, value: T) { if (self.length == self.capacity) { self.grow(); diff --git a/std/maybe.jam b/std/maybe.jam index aec5a61..7a1d1da 100644 --- a/std/maybe.jam +++ b/std/maybe.jam @@ -17,12 +17,10 @@ // // Use cases: // - Two-stage construction: declare a slot, fill it, call assumeInit. -// - `Buf(Maybe(T)).allocUninit(n)` + `assumeInitBuf` — a fixed-size -// heap slice that's allocated uninitialised, filled (per-slot writes, -// fread, etc.), then type-promoted to `Buf(T)` exactly once. The -// Rust pattern: `Box::new_uninit_slice(n)` → fill → `.assume_init()`. - -const { Buf } = import("std/buf"); +// - TODO: a `Vec(Maybe(T))` fast-path (allocate-uninit + per-slot +// write + `Maybe(T).assumeInitVec(v) -> Vec(T)`) would mirror Rust's +// `Box::new_uninit_slice(n)` → fill → `.assume_init()`. Needs a +// `Vec.allocUninit(n)` constructor first. pub fn Maybe(T: type) type { return struct { @@ -32,37 +30,12 @@ pub fn Maybe(T: type) type { // call `assumeInit()`). value: T, - // Wrap an already-initialised T in a Maybe slot. Useful for the - // `buf[i] = Maybe(T).init(v)` write pattern. + // Wrap an already-initialised T in a Maybe slot. pub fn init(v: T) Self { return Self { value: v }; } // Read the contained T. Caller asserts the backing storage has // been written with a valid T — UB if not. jam has no init- // tracking; this is your contract. pub fn assumeInit(self: Self) T { return self.value; } - - // Consume a `Buf(Maybe(T))` and return a `Buf(T)` over the same - // memory, asserting that every slot has been written with a - // valid T. UB if any slot is still uninitialised. Called as - // `Maybe(u8).assumeInitBuf(raw)` — T is bound by the Maybe(u8) - // type-constructor dispatch (the same mechanism `Vec(u32).empty()` - // uses to bind its T). - // - // Ownership transfer: `b: move` hands b to this function, AND we - // explicitly disarm b by zeroing `ptr`/`length`. `Buf.drop`'s - // null guard makes the disarmed source's drop a no-op when its - // scope ends. The new `Buf(T)` is the sole owner of the storage. - pub fn assumeInitBuf(b: move Buf(Self)) Buf(T) { - // Layout: Maybe(T) is a single-field struct wrapping T, so - // `*mut[] Maybe(T)` and `*mut[] T` alias the same bytes. jam's - // `as` requires a `*mut[] u8` bridge between typed pointers - // (same shape Vec uses for malloc'd storage). Ownership of the - // storage transfers via `b: move` — jam's drop registry treats - // the consumed b as no longer in scope, so its drop doesn't - // fire; the new Buf(T) is the sole owner. - var bytePtr: *mut[] u8 = b.ptr as *mut[] u8; - var length: u32 = b.length; - return Buf(T).fromRaw(bytePtr as *mut[] T, length); - } }; } diff --git a/tests/unit/test_buf.jam b/tests/unit/test_buf.jam deleted file mode 100644 index 31f14bd..0000000 --- a/tests/unit/test_buf.jam +++ /dev/null @@ -1,65 +0,0 @@ -// std.buf — Buf(T) unit tests. -// -// Buf is always initialised — there's no uninit alloc form. The sole -// constructor is `filled(v, n)`, which fills every slot with `v` via typed -// writes (safe for any T). Drop firing on scope exit is implicit — the -// test passing without crashing means the drop registry hooked cfn drop. -// -const { assert } = import("test"); -const { Buf } = import("std/buf"); - -extern fn memset(dst: *mut[] u8, c: i32, n: u64) *mut[] u8; - -const Point = struct { x: i32, y: i32 }; - -// Length tag is set at construction. -tfn bufLenAfterFilled() { - const b: Buf(u8) = Buf(u8).filled(0, 64); - assert(b.len(), 64); -} - -// `filled(v, n)` writes `v` into every slot (typed; safe for any T). -tfn bufFilledIsValue() { - const b: Buf(u32) = Buf(u32).filled(7, 4); - assert(b[0], 7); - assert(b[1], 7); - assert(b[2], 7); - assert(b[3], 7); -} - -// `buf[i] = v` then `buf[i]` round-trips every slot (primitive T). -tfn bufIndexRoundtrip() { - var b: Buf(u32) = Buf(u32).filled(0, 4); - b[0] = 11; - b[1] = 22; - b[2] = 33; - b[3] = 44; - assert(b[0], 11); - assert(b[1], 22); - assert(b[2], 33); - assert(b[3], 44); -} - -// `.raw()` aliases the same allocation as `[i]` — memset through the byte -// pointer is observable on the typed read. -tfn bufRawFFIRoundtrip() { - var b: Buf(u8) = Buf(u8).filled(0, 8); - memset(b.raw(), 0xAB, 8); - assert(b[0], 0xAB); - assert(b[7], 0xAB); -} - -// Struct elements — write via `[i] = v`, read via `[i]` (the JirTag::Index -// codegen now returns the GEP pointer for byref element types, so a -// struct read through `cfn at` lands in the sret slot directly). -tfn bufStructElement() { - var b: Buf(Point) = Buf(Point).filled(Point { x: 0, y: 0 }, 2); - b[0] = Point { x: 1, y: 2 }; - b[1] = Point { x: 3, y: 4 }; - const p0: Point = b[0]; - const p1: Point = b[1]; - assert(p0.x, 1); - assert(p0.y, 2); - assert(p1.x, 3); - assert(p1.y, 4); -} diff --git a/tests/unit/test_maybe.jam b/tests/unit/test_maybe.jam index a1426af..51e55ce 100644 --- a/tests/unit/test_maybe.jam +++ b/tests/unit/test_maybe.jam @@ -1,12 +1,10 @@ // std.maybe — Maybe(T) tests. // -// Single-value init / assumeInit round-trip. The Buf-upgrade pattern -// (`Maybe(T).assumeInitBuf(buf)`) remains in std/maybe.jam as a future- -// proofing hook, but Buf is currently always-initialised (no public uninit -// alloc form), so there's no ergonomic way to construct a `Buf(Maybe(T))` -// to upgrade. When jam grows a `Buf.fromRaw`-based uninit construction -// helper or a `Vec(T).intoBuf` ownership-transfer path, those tests can -// be added back. +// Single-value init / assumeInit round-trip. The Vec-upgrade pattern +// (`Maybe(T).assumeInitVec(vec)` — Rust's `Box::new_uninit_slice + assume_init`) +// is TODO in std/maybe.jam; it needs a `Vec.allocUninit(n)` constructor +// first. When that lands, add a test that allocates Vec(Maybe(T)), fills +// every slot, and round-trips to Vec(T). const { assert } = import("test"); const { Maybe } = import("std/maybe");