diff --git a/src/abi.cpp b/src/abi.cpp index 295f358..cffc7d5 100644 --- a/src/abi.cpp +++ b/src/abi.cpp @@ -34,6 +34,13 @@ bool isByRef(TypeIdx ty, const JamCodegenContext &ctx) { TypeIdx resolved = ctx.resolveGenericCall(ty); return resolved != kNoType && resolved != ty && isByRef(resolved, ctx); } + // Deferred-length arrays classify exactly like their resolved + // Array form (byref) — resolve so the recursion also validates + // the length expression on first ABI consultation. + case TypeKind::ArrayExpr: { + TypeIdx resolved = ctx.resolveArrayExpr(ty); + return resolved != kNoType && resolved != ty && isByRef(resolved, ctx); + } // Scalars and pointer-shaped values: byval. The whole value // rides in registers; SSA representation is the value itself. case TypeKind::Bool: diff --git a/src/ast_flat.h b/src/ast_flat.h index b805942..165dfd8 100644 --- a/src/ast_flat.h +++ b/src/ast_flat.h @@ -336,6 +336,14 @@ enum class TypeKind : uint8_t { PtrMany, // [*]T — ptrT.elem (indexable) Slice, // []T — sliceT.elem (lowered to {ptr, len} struct) Array, // [N]T — arrayT.elem, arrayT.len + // Parser-emitted "fixed-size array, length resolution deferred." The + // parser sees `[expr]T` where `expr` is not a plain integer literal + // (`[SIZE]u8`, `[2 * 1024]u8`) and can't evaluate it — module consts + // aren't registered until semantic time. The TypeKey carries the + // element TypeIdx in `a` and the length expression's NodeIdx in `b`. + // The codegen comptime-folds the expression lazily (mirroring + // GenericCall) and canonicalizes to a plain Array key. + ArrayExpr, // arrayExprT.elem, arrayExprT.lenExpr (NodeIdx) Struct, // structT.name (StringIdx) Enum, // enumT.name (StringIdx); see EnumDeclAST for variants Union, // unionT.name (StringIdx); see UnionDeclAST for fields @@ -392,6 +400,7 @@ struct TypeKey { // PtrSingle / PtrMany: a = elem TypeIdx // Slice: a = elem TypeIdx // Array: a = elem TypeIdx, b = length + // ArrayExpr: a = elem TypeIdx, b = length expr NodeIdx // Struct: a = StringIdx (struct name) }; @@ -412,6 +421,7 @@ inline bool operator==(const TypeKey &x, const TypeKey &y) { case TypeKind::Slice: return x.a == y.a; case TypeKind::Array: + case TypeKind::ArrayExpr: return x.a == y.a && x.b == y.b; case TypeKind::Struct: case TypeKind::Enum: @@ -545,6 +555,12 @@ class TypePool { TypeIdx internArray(TypeIdx elem, uint32_t len) { return intern(TypeKey{TypeKind::Array, 0, 0, elem, len}); } + // `[expr]T` with a non-literal length: the length expression's + // NodeIdx rides in `b` until codegen comptime-folds it (see + // JamCodegenContext::resolveArrayExpr). + TypeIdx internArrayExpr(TypeIdx elem, NodeIdx lenExpr) { + return intern(TypeKey{TypeKind::ArrayExpr, 0, 0, elem, lenExpr}); + } TypeIdx internStruct(StringIdx nameId) { return intern(TypeKey{TypeKind::Struct, 0, 0, nameId, 0}); } diff --git a/src/astgen.cpp b/src/astgen.cpp index bac9236..cd35d17 100644 --- a/src/astgen.cpp +++ b/src/astgen.cpp @@ -158,6 +158,19 @@ static void appendErrorHere(AstGenCtx &gctx, std::string message) { appendErrorNode(gctx, gctx.currentNode, std::move(message)); } +// Canonicalize a parser-deferred `[expr]T` (TypeKind::ArrayExpr) before +// the type becomes astgen state — binding types, expected hints, field +// reads. The codegen-side resolver throws when the length expression +// doesn't comptime-fold; convert that into a per-decl diagnostic +// anchored at the current node so sibling decls keep compiling. +static TypeIdx resolveDeferredArrayTy(AstGenCtx &gctx, TypeIdx ty) { + if (ty == kNoType) return ty; + if (gctx.ctx.getTypePool().get(ty).kind != TypeKind::ArrayExpr) return ty; + try { + return gctx.ctx.resolveArrayExpr(ty); + } catch (const std::exception &e) { failHere(gctx, e.what()); } +} + // Forward-declared so the recovery helpers can build Poison; the // real definition follows below alongside `emitAllocaHoisted`. static JirRef emit(AstGenCtx &gctx, JirInst inst); @@ -775,7 +788,11 @@ static void astgenVarDecl(AstGenCtx &gctx, const AstNode &n) { gctx.localTypes[name] = type; if (!gctx.localScopes.empty()) { gctx.localScopes.back().insert(name); } } else { - type = declared; + // Deferred `[expr]T` annotations canonicalize here so the + // binding's type — and everything downstream that switches on + // TypeKind::Array (index lowering, literal-length checks) — + // sees the resolved length. + type = resolveDeferredArrayTy(gctx, declared); JirInst alloca{}; alloca.tag = JirTag::Alloca; alloca.ty = type; @@ -1231,7 +1248,8 @@ static JirRef astgenStructLit(AstGenCtx &gctx, const AstNode &n, appendErrorHere(gctx, "unknown struct field `" + fieldName + "`"); continue; } - TypeIdx expectedField = info->fields[idx].second; + TypeIdx expectedField = + resolveDeferredArrayTy(gctx, info->fields[idx].second); JirRef fieldVal = astgenExpr(gctx, exprIdx, expectedField); // Struct-literal field capture is a MOVE for drop-bearing types: // if the field's value came from a tracked local Variable, mark @@ -1343,7 +1361,8 @@ static void astgenStructLitInto(AstGenCtx &gctx, const AstNode &n, failHere(gctx, "astgen: struct literal missing field `" + info->fields[i].first + "`"); } - TypeIdx expectedField = info->fields[i].second; + TypeIdx expectedField = + resolveDeferredArrayTy(gctx, info->fields[i].second); TypeIdx fieldPtrTy = gctx.ctx.getTypePool().intern( TypeKey{TypeKind::PtrSingle, 0, 0, expectedField, 0}); JirInst fieldAddr{}; @@ -1413,6 +1432,7 @@ static bool astgenExprIntoPtr(AstGenCtx &gctx, NodeIdx exprIdx, case AstTag::ArrayLit: { // Per-element write into destPtr. Element type comes from // the surrounding context or from compiling the first elem. + expectedTy = resolveDeferredArrayTy(gctx, expectedTy); TypeIdx elemTy = static_cast(n.lhs); if (elemTy == kNoType && expectedTy != kNoType) { const TypeKey &ek = gctx.ctx.getTypePool().get(expectedTy); @@ -1423,6 +1443,17 @@ static bool astgenExprIntoPtr(AstGenCtx &gctx, NodeIdx exprIdx, if (elemTy == kNoType) return false; ExtraIdx elemsExtra = static_cast(n.rhs); uint32_t count = ns.getExtra(elemsExtra); + // Destination is a fixed-size array: the literal must supply + // exactly that many elements, or the per-element stores below + // would write past (or short of) the destination slot. + if (expectedTy != kNoType) { + const TypeKey &ek = gctx.ctx.getTypePool().get(expectedTy); + if (ek.kind == TypeKind::Array && ek.b != count) { + failHere(gctx, "array literal has " + std::to_string(count) + + " element(s) but the array type expects " + + std::to_string(ek.b)); + } + } TypeIdx u64Ty = BuiltinType::U64; TypeIdx elemPtrTy = gctx.ctx.getTypePool().intern( TypeKey{TypeKind::PtrSingle, 0, 0, elemTy, 0}); @@ -1582,7 +1613,8 @@ static JirRef astgenMemberAccess(AstGenCtx &gctx, const AstNode &n) { info->name + "`", kNoType); } - TypeIdx fieldTy = info->fields[idx].second; + TypeIdx fieldTy = + resolveDeferredArrayTy(gctx, info->fields[idx].second); TypeIdx fieldPtrTy = gctx.ctx.getTypePool().intern( TypeKey{TypeKind::PtrSingle, 0, 0, fieldTy, 0}); JirInst fa{}; @@ -1670,7 +1702,7 @@ static JirRef astgenMemberAccess(AstGenCtx &gctx, const AstNode &n) { inst.tag = JirTag::FieldAccess; inst.a = baseRef; inst.b = static_cast(idx); - inst.ty = info->fields[idx].second; + inst.ty = resolveDeferredArrayTy(gctx, info->fields[idx].second); return emit(gctx, inst); } @@ -1680,6 +1712,7 @@ static JirRef astgenMemberAccess(AstGenCtx &gctx, const AstNode &n) { static JirRef astgenArrayLit(AstGenCtx &gctx, const AstNode &n, TypeIdx expected) { const NodeStore &ns = gctx.ctx.getNodeStore(); + expected = resolveDeferredArrayTy(gctx, expected); TypeIdx elemTy = static_cast(n.lhs); if (elemTy == kNoType && expected != kNoType) { const TypeKey &ek = gctx.ctx.getTypePool().get(expected); @@ -1688,6 +1721,19 @@ static JirRef astgenArrayLit(AstGenCtx &gctx, const AstNode &n, ExtraIdx elemsExtra = static_cast(n.rhs); uint32_t count = ns.getExtra(elemsExtra); + // When the destination type is a fixed-size array, the literal must + // supply exactly that many elements. Without this check the literal + // keeps its own length and the store writes past (or short of) the + // destination slot. + if (expected != kNoType) { + const TypeKey &ek = gctx.ctx.getTypePool().get(expected); + if (ek.kind == TypeKind::Array && ek.b != count) { + failHere(gctx, "array literal has " + std::to_string(count) + + " element(s) but the array type expects " + + std::to_string(ek.b)); + } + } + std::vector elems; elems.reserve(count); for (uint32_t i = 0; i < count; i++) { @@ -1726,25 +1772,56 @@ static JirRef astgenArrayRepeat(AstGenCtx &gctx, const AstNode &n, const NodeStore &ns = gctx.ctx.getNodeStore(); TypeIdx arrTy = static_cast(n.lhs); if (arrTy == kNoType) arrTy = expected; + arrTy = resolveDeferredArrayTy(gctx, arrTy); ExtraIdx extra = static_cast(n.rhs); NodeIdx valueIdx = static_cast(ns.getExtra(extra)); NodeIdx countIdx = static_cast(ns.getExtra(extra + 1)); + // Literal counts read straight off the node; anything else + // (`[0; SIZE]`, `[0; 2 * 1024]`) comptime-folds against the + // module-const scope — mirroring the deferred `[expr]T` type + // position so both spellings stay in sync. const AstNode &cn = ns.get(countIdx); - if (cn.tag != AstTag::NumberLit) { - failHere( - gctx, - "astgen: array-repeat count must be a constant integer literal"); + uint64_t count = 0; + if (cn.tag == AstTag::NumberLit) { + count = static_cast(cn.lhs) | + (static_cast(cn.rhs) << 32); + } else { + jam::ComptimeValue v = gctx.ctx.foldComptimeExpr(countIdx); + if (v.isNone()) { + failHere(gctx, "array-repeat count must be comptime-known"); + } + if (!v.isInt() || (v.intVal.isSigned && v.asI64() < 0)) { + failHere(gctx, "array-repeat count must be a non-negative integer"); + } + count = v.asU64(); + } + if (count > UINT32_MAX) { + // A negative fold wraps to a huge unsigned value; report it as + // the sign error it is rather than a baffling range overflow. + if (static_cast(count) < 0) { + failHere(gctx, "array-repeat count must be a non-negative integer"); + } + failHere(gctx, "array repeat count " + std::to_string(count) + + " exceeds u32 range"); } - uint64_t count = - static_cast(cn.lhs) | (static_cast(cn.rhs) << 32); // Resolve element type. From the array TypeKey if we have it; else - // from the first compile of the value. + // from the first compile of the value. When the destination is a + // fixed-size array, the repeat count must match its length exactly — + // the memset/store below sizes from the count, so a mismatch writes + // past (or short of) the destination slot. TypeIdx elemTy = kNoType; if (arrTy != kNoType) { const TypeKey &k = gctx.ctx.getTypePool().get(arrTy); - if (k.kind == TypeKind::Array) elemTy = static_cast(k.a); + if (k.kind == TypeKind::Array) { + elemTy = static_cast(k.a); + if (k.b != count) { + failHere(gctx, "array repeat count " + std::to_string(count) + + " does not match array type length " + + std::to_string(k.b)); + } + } } JirRef val = astgenExpr(gctx, valueIdx, elemTy); if (elemTy == kNoType) elemTy = gctx.jfn.getInst(val).ty; @@ -3453,7 +3530,9 @@ static JirRef astgenMatch(AstGenCtx &gctx, const AstNode &n, TypeIdx expected) { std::unordered_set seen; switchCases.erase( std::remove_if(switchCases.begin(), switchCases.end(), - [&](const SwitchCase &c) { return !seen.insert(c.value).second; }), + [&](const SwitchCase &c) { + return !seen.insert(c.value).second; + }), switchCases.end()); } @@ -4417,7 +4496,7 @@ static JirRef emitCall(AstGenCtx &gctx, const FunctionAST *fn, } else { JirInst a{}; a.tag = JirTag::Alloca; - a.ty = fn->ReturnType; + a.ty = resolveDeferredArrayTy(gctx, fn->ReturnType); sretSlot = emitAllocaHoisted(gctx, a); } allArgs.push_back(sretSlot); @@ -4432,7 +4511,7 @@ static JirRef emitCall(AstGenCtx &gctx, const FunctionAST *fn, call.tag = JirTag::Call; call.a = calleeId; call.b = extra; - call.ty = fn->ReturnType; + call.ty = resolveDeferredArrayTy(gctx, fn->ReturnType); JirRef callRef = emit(gctx, call); // Calling a `noreturn` function diverges. Terminate the current // block with Unreachable so downstream code is dead and the JIR @@ -5768,7 +5847,7 @@ static JirRef astgenExpr(AstGenCtx &gctx, NodeIdx node, TypeIdx expected, failHere(gctx, "astgen: unknown field `" + memberName + "` on `" + info->name + "`"); } - leafTy = info->fields[idx].second; + leafTy = resolveDeferredArrayTy(gctx, info->fields[idx].second); TypeIdx ptrTy = gctx.ctx.getTypePool().intern( TypeKey{TypeKind::PtrSingle, 0, 0, leafTy, 0}); JirInst fa{}; @@ -6005,6 +6084,12 @@ void astgenBodyInto(JirFunction &jfn, const FunctionAST &fn, // the function top level drops here at function exit. pushDropScope(gctx); + // A deferred `[expr]T` return annotation canonicalizes before any + // return statement compares its value type against it. Runs before + // jirDeclarePrototype reads jfn.returnType, so the LLVM signature + // sees the resolved length too. + jfn.returnType = resolveDeferredArrayTy(gctx, jfn.returnType); + // Lower each parameter. The ABI classifier is the single source of // truth — both the prototype emitter and the call-site argument // lowering ask the same `classifyParam(mode, type)` question, so @@ -6021,22 +6106,25 @@ void astgenBodyInto(JirFunction &jfn, const FunctionAST &fn, // pointer-to-pointee rather than a by-value Param. for (size_t i = 0; i < fn.Args.size(); i++) { const Param &p = fn.Args[i]; - jam::abi::ParamABI pabi = jam::abi::classifyParam(p.Mode, p.Type, ctx); + // Deferred `[expr]T` param annotations canonicalize before the + // type is stamped on the Param inst / local binding. + TypeIdx pTy = resolveDeferredArrayTy(gctx, p.Type); + jam::abi::ParamABI pabi = jam::abi::classifyParam(p.Mode, pTy, ctx); bool byPtr = pabi.kind == jam::abi::ParamABI::Kind::ByPointer; JirInst paramInst{}; paramInst.tag = JirTag::Param; paramInst.a = static_cast(i); - paramInst.ty = p.Type; + paramInst.ty = pTy; if (byPtr) paramInst.flags |= 1; JirRef paramRef = emit(gctx, paramInst); if (byPtr) { gctx.locals[p.Name] = paramRef; - gctx.localTypes[p.Name] = p.Type; + gctx.localTypes[p.Name] = pTy; } else { JirInst alloca{}; alloca.tag = JirTag::Alloca; - alloca.ty = p.Type; + alloca.ty = pTy; JirRef allocaRef = emitAllocaHoisted(gctx, alloca); JirInst store{}; store.tag = JirTag::Store; @@ -6044,7 +6132,7 @@ void astgenBodyInto(JirFunction &jfn, const FunctionAST &fn, store.b = paramRef; emit(gctx, store); gctx.locals[p.Name] = allocaRef; - gctx.localTypes[p.Name] = p.Type; + gctx.localTypes[p.Name] = pTy; } } diff --git a/src/codegen.cpp b/src/codegen.cpp index 02160aa..381df96 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -147,6 +147,13 @@ JamTypeRef JamCodegenContext::getLLVMType(TypeIdx ty) const { result = JamLLVMArrayType(elem, k.b); break; } + case TypeKind::ArrayExpr: { + // lazily fold the length expression to a canonical Array + // key, then recurse. The resolution is memoized in + // arrayExprResolutions_ (same shape as GenericCall below). + result = getLLVMType(resolveArrayExpr(ty)); + break; + } case TypeKind::Struct: { const std::string &name = stringPool.get(static_cast(k.a)); const auto *sinfo = getStruct(name); @@ -663,6 +670,8 @@ uint64_t JamCodegenContext::typeSize(TypeIdx ty) const { return 16; // (ptr, len) case TypeKind::Array: return static_cast(k.b) * typeSize(static_cast(k.a)); + case TypeKind::ArrayExpr: + return typeSize(resolveArrayExpr(ty)); case TypeKind::Struct: case TypeKind::Named: { // a Named type may be a substitution-context @@ -784,6 +793,8 @@ uint64_t JamCodegenContext::typeAlign(TypeIdx ty) const { return 8; case TypeKind::Array: return typeAlign(static_cast(k.a)); + case TypeKind::ArrayExpr: + return typeAlign(resolveArrayExpr(ty)); case TypeKind::Struct: case TypeKind::Named: { // substitution context wins. A Named type may @@ -886,6 +897,12 @@ TypeIdx substituteType(TypeIdx ty, return types.internArray( substituteType(static_cast(k.a), subst, types, strings), k.b); + case TypeKind::ArrayExpr: + // Substitute the element type; the length expression is + // type-free and rides along unchanged. + return types.internArrayExpr( + substituteType(static_cast(k.a), subst, types, strings), + static_cast(k.b)); case TypeKind::GenericCall: { // Recurse into args: a generic call inside a generic body // (e.g. `Box(Maybe(T))`) substitutes T in the inner call. @@ -1078,6 +1095,59 @@ TypeIdx JamCodegenContext::resolveGenericCall(TypeIdx callTy) const { return result; } +jam::ComptimeValue JamCodegenContext::foldComptimeExpr(NodeIdx expr) 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. + jam::ComptimeEvaluator ev(nodeStore, stringPool, typePool); + jam::ComptimeScope scope; + bool progress = true; + while (progress) { + progress = false; + for (const auto &kv : moduleConsts) { + if (scope.lookup(kv.first) != nullptr) continue; + jam::ComptimeValue v = ev.eval(kv.second.initExpr, scope); + if (!v.isNone()) { + scope.bind(kv.first, v); + progress = true; + } + } + } + return ev.eval(expr, scope); +} + +TypeIdx JamCodegenContext::resolveArrayExpr(TypeIdx ty) const { + auto cached = arrayExprResolutions_.find(ty); + if (cached != arrayExprResolutions_.end()) return cached->second; + + const TypeKey &k = typePool.get(ty); + jam::ComptimeValue len = foldComptimeExpr(static_cast(k.b)); + if (len.isNone()) { + throw std::runtime_error("array length must be comptime-known"); + } + if (!len.isInt() || (len.intVal.isSigned && len.asI64() < 0)) { + throw std::runtime_error("array size must be a non-negative integer"); + } + uint64_t lenVal = len.asU64(); + if (lenVal > UINT32_MAX) { + // A negative fold wraps to a huge unsigned value; report it as + // the sign error it is rather than a baffling range overflow. + if (static_cast(lenVal) < 0) { + throw std::runtime_error( + "array size must be a non-negative integer"); + } + throw std::runtime_error("array size " + std::to_string(lenVal) + + " exceeds u32 range"); + } + + TypeIdx resolved = typePool.internArray(static_cast(k.a), + static_cast(lenVal)); + arrayExprResolutions_.emplace(ty, resolved); + return resolved; +} + // Instantiate a `struct {...}` expression appearing in a generic body's // return statement. Substitutes each field's TypeIdx with the concrete // generic args, creates a fresh LLVM struct type with a unique name, and @@ -1292,6 +1362,9 @@ TypeIdx JamCodegenContext::instantiateStructExpr( TypeIdx r = cc->resolveGenericCall(t); if (r != kNoType) return r; } + if (k.kind == TypeKind::ArrayExpr) { + return cc->resolveArrayExpr(t); + } return t; }, &mutCtx); diff --git a/src/codegen.h b/src/codegen.h index a76ed7c..19bd00c 100644 --- a/src/codegen.h +++ b/src/codegen.h @@ -400,6 +400,12 @@ class JamCodegenContext { // qualified `c.Vec(i32)` both resolve to the same Vec FunctionAST // and produce a single Vec__i32 struct). mutable std::unordered_map genericResolutions_; + + // `arrayExprResolutions_` memoizes deferred array lengths: every + // unique `TypeKind::ArrayExpr` TypeIdx maps to the canonical + // `TypeKind::Array` TypeIdx produced by comptime-folding its length + // expression. Same shape as genericResolutions_. + mutable std::unordered_map arrayExprResolutions_; struct GenericInstanceKey { const FunctionAST *fn; std::vector args; @@ -471,6 +477,21 @@ class JamCodegenContext { // same TypeIdx hit the cache and return the same concrete TypeIdx. TypeIdx resolveGenericCall(TypeIdx callTy) const; + // resolve a `TypeKind::ArrayExpr` TypeIdx (`[SIZE]u8`, + // `[2 * 1024]u8`) to a canonical `TypeKind::Array` by + // comptime-folding the length expression against the module-const + // scope. Throws with a precise message when the expression doesn't + // fold to a comptime-known non-negative integer that fits u32. + // Result is memoized in arrayExprResolutions_. + TypeIdx resolveArrayExpr(TypeIdx ty) const; + + // Comptime-fold an arbitrary expression node against the + // module-const scope (consts may chain; the scope is seeded to a + // fixpoint). Returns a None value when the expression depends on + // anything not comptime-known. Shared by resolveArrayExpr and the + // array-repeat count fold in astgen. + jam::ComptimeValue foldComptimeExpr(NodeIdx expr) const; + // register the anonymous-struct table for the current // module so the substitution engine can find struct expression // bodies by their AnonStructs index. diff --git a/src/main.cpp b/src/main.cpp index 90567b3..75abab6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -16,6 +17,9 @@ #include #include #include +#ifndef _WIN32 +#include +#endif #include "analyzer.h" #include "ast.h" @@ -528,25 +532,6 @@ static int compileAndRun(const std::string &filename, } } - // Single demand-driven body-fill pass: walk every Struct/Enum/ - // Union decl in the DeclTable and ask the analyzer to materialise - // it. The per-kind `resolveTypeFields*` functions are re-entrant - // — when a struct's field references another struct/enum/union, - // the field walk's ensure*Body call fills that dependency - // transitively. The publicOnly filtering that the per-module - // fill*Bodies lambdas used to do isn't needed any more because - // `registerTopLevelDecls` already applied that filter when - // populating the DeclTable. - auto &dt = codegenCtx.declTable(); - for (std::size_t i = 1; i < dt.all().size(); ++i) { - jam::DeclIndex idx = static_cast(i); - const jam::Decl &dr = dt.get(idx); - if (dr.kind == jam::DeclKind::Struct || - dr.kind == jam::DeclKind::Enum || dr.kind == jam::DeclKind::Union) { - codegenCtx.analyzer().ensureDeclAnalyzed(idx); - } - } - // Register module-scope `const NAME[: T]? = expr;` bindings. These // are inlined at use sites (see AstTag::Variable in ast.cpp), so we // only need to teach the codegen context about them — no LLVM @@ -664,6 +649,29 @@ static int compileAndRun(const std::string &filename, } registerConsts(module.get()); + // Single demand-driven body-fill pass: walk every Struct/Enum/ + // Union decl in the DeclTable and ask the analyzer to materialise + // it. The per-kind `resolveTypeFields*` functions are re-entrant + // — when a struct's field references another struct/enum/union, + // the field walk's ensure*Body call fills that dependency + // transitively. The publicOnly filtering that the per-module + // fill*Bodies lambdas used to do isn't needed any more because + // `registerTopLevelDecls` already applied that filter when + // populating the DeclTable. + // + // Runs AFTER module-const registration: a field typed `[SIZE]u8` + // comptime-folds SIZE against the const scope when getLLVMType + // resolves the deferred array length. + auto &dt = codegenCtx.declTable(); + for (std::size_t i = 1; i < dt.all().size(); ++i) { + jam::DeclIndex idx = static_cast(i); + const jam::Decl &dr = dt.get(idx); + if (dr.kind == jam::DeclKind::Struct || + dr.kind == jam::DeclKind::Enum || dr.kind == jam::DeclKind::Union) { + codegenCtx.analyzer().ensureDeclAnalyzed(idx); + } + } + // Two-pass codegen: declare every function's prototype first, then // emit bodies. Without this, calling a function defined later in the // file (or another module) would fail with "Unknown function". The @@ -934,6 +942,9 @@ static int compileAndRun(const std::string &filename, TypeIdx r = cc->resolveGenericCall(t); if (r != kNoType) return r; } + if (k.kind == TypeKind::ArrayExpr) { + return cc->resolveArrayExpr(t); + } return t; }, &codegenCtx); @@ -1235,6 +1246,22 @@ static int compileAndRun(const std::string &filename, #ifdef _WIN32 return exitCode; #else + if (exitCode == -1) { + std::cerr << "Failed to run " << outputName << std::endl; + return 1; + } + // A signal-killed child (segfault, abort, …) has no exit status; + // WEXITSTATUS on it reads garbage bits that decode to 0, which + // would report a crashed test binary as "passed" — and since the + // crash also loses the child's unflushed stdout, the failure + // would be completely silent. Follow the shell convention: + // 128 + signal number. + if (WIFSIGNALED(exitCode)) { + int sig = WTERMSIG(exitCode); + std::cerr << outputName << " terminated by signal " << sig << " (" + << strsignal(sig) << ")" << std::endl; + return 128 + sig; + } return WEXITSTATUS(exitCode); #endif } diff --git a/src/parser.cpp b/src/parser.cpp index d4e831d..014acf3 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -8,6 +8,7 @@ #include "parser.h" #include "jam_llvm.h" #include "number_literal.h" +#include #include #include @@ -687,11 +688,40 @@ TypeIdx Parser::parseType() { return typePool->internSlice(parseType()); } // `[N]T` — fixed-size array. No tag. - consume(TOK_NUMBER, "Expected size or `]` after `[`"); - uint32_t len = static_cast( - std::stoul(std::string(previous().text(source_)))); + // + // Literal fast path: a plain integer size (`[0x800]u8`) interns + // the Array key right here. The token runs through the same + // number-literal parser as value-position literals so hex / + // binary / octal / underscore forms resolve correctly — + // `std::stoul` would silently stop at the `x` and produce a + // zero-length array. + if (check(TOK_NUMBER) && + current + 1 < static_cast(tokens.size()) && + tokens[current + 1].type == TOK_CLOSE_BRACKET) { + advance(); + bool sizeNeg = false; + bool sizeFloat = false; + uint64_t sizeVal = + parseNumLexeme(previous().text(source_), sizeNeg, sizeFloat); + if (sizeNeg || sizeFloat) { + parseError("array size must be a non-negative integer"); + } + if (sizeVal > UINT32_MAX) { + parseError("array size `" + + std::string(previous().text(source_)) + + "` exceeds u32 range"); + } + uint32_t len = static_cast(sizeVal); + consume(TOK_CLOSE_BRACKET, "Expected `]` after array size"); + return typePool->internArray(parseType(), len); + } + // Expression path: `[SIZE]u8`, `[2 * 1024]u8` — module consts + // aren't known yet, so park the expression node in a deferred + // ArrayExpr key; codegen comptime-folds it on first use + // (mirroring GenericCall resolution). + NodeIdx lenExpr = parseExpression(); consume(TOK_CLOSE_BRACKET, "Expected `]` after array size"); - return typePool->internArray(parseType(), len); + return typePool->internArrayExpr(parseType(), lenExpr); } if (match(TOK_TYPE)) { std::string_view s = previous().text(source_); diff --git a/tests/cpp/test_codegen_errors.cpp b/tests/cpp/test_codegen_errors.cpp index 1fb15ae..10df74d 100644 --- a/tests/cpp/test_codegen_errors.cpp +++ b/tests/cpp/test_codegen_errors.cpp @@ -15,6 +15,7 @@ #include "test_framework.h" #include #include +#include #include #include #include @@ -61,6 +62,31 @@ bool stderrContains(const CompileResult &r, const std::string &substr) { return r.stderr_.find(substr) != std::string::npos; } +// `jam.out test` variant: compiles AND RUNS the file's tfn tests, so it +// exercises the test-runner harness itself (exit-status decoding of the +// spawned binary included). Runs from /tmp so the default output name +// (`./output`) can't collide with the build tree's `output/` directory. +CompileResult runTestMode(const std::string &name, const std::string &source) { + std::string path = "/tmp/" + name + ".jam"; + { + std::ofstream out(path); + out << source; + } + std::string jamBin = + (std::filesystem::current_path() / "output" / "jam.out").string(); + std::string cmd = "cd /tmp && " + jamBin + " test " + name + ".jam 2>&1"; + + std::string output; + FILE *pipe = popen(cmd.c_str(), "r"); + if (!pipe) { throw std::runtime_error("popen failed: " + cmd); } + char buf[256]; + while (fgets(buf, sizeof(buf), pipe) != nullptr) output += buf; + int status = pclose(pipe); + + int exitCode = WIFEXITED(status) ? WEXITSTATUS(status) : -1; + return {exitCode, std::move(output)}; +} + // `--emit-ir` variant. Skips the link step (so it doesn't try to drop // the binary into `./output`, which is the build directory) and pipes // LLVM IR back through the same stdout/stderr channel. Positive tests @@ -188,6 +214,37 @@ class CodegenErrorTests { framework.addTest( "XMod - imported body cannot see the entry module's imports", testImportedBodyCannotSeeEntryImports); + // Array sizes in type position + literal-length agreement. + framework.addTest( + "Array - hex size in type position lowers to [2048 x i8]", + testHexArraySizeLowersCorrectly); + framework.addTest("Array - repeat count != array length rejected", + testArrayRepeatCountMismatchRejected); + framework.addTest("Array - list literal longer than array rejected", + testArrayLitTooLongRejected); + framework.addTest("Array - list literal shorter than array rejected", + testArrayLitTooShortRejected); + framework.addTest("Array - float array size rejected", + testFloatArraySizeRejected); + framework.addTest("Array - array size beyond u32 rejected", + testHugeArraySizeRejected); + framework.addTest("Array - empty literal into sized array rejected", + testEmptyLitIntoSizedArrayRejected); + framework.addTest("Array - size literal grammar matches value position", + testArraySizeLiteralGrammarParity); + framework.addTest("Array - const size folds to [2048 x i8] in IR", + testConstArraySizeLowersCorrectly); + framework.addTest("Array - non-comptime size rejected", + testNonComptimeSizeRejected); + framework.addTest("Array - negative const size rejected", + testNegativeConstSizeRejected); + framework.addTest("Array - float const size rejected", + testFloatConstSizeRejected); + framework.addTest("Array - length mismatch via const size rejected", + testConstSizeMismatchRejected); + framework.addTest( + "Harness - signal-killed test binary reported as failure", + testSignalKilledBinaryReported); } private: @@ -616,6 +673,226 @@ pub fn makeThing(s: *mut u32) Thing { return Thing { sink: s }; } ASSERT_TRUE(stderrContains(r, "lib.jam:")); ASSERT_TRUE(!stderrContains(r, "main.jam:")); } + + // `[0x800]u8` must intern a 2048-element array type. The size token + // goes through the full number-literal parser; a base-10-only scan + // (std::stoul) stops at the `x` and silently produces `[0]u8`, after + // which the repeat literal memsets 2048 bytes into a 0-byte slot. + static void testHexArraySizeLowersCorrectly() { + auto r = compileSourceIR("array_hex_size_ir", R"( +const Block = struct { + cells: [0x800]u8, +}; +fn main() { + var b = Block{cells: [0; 0x800]}; + b.cells[0] = 1; +} +)"); + ASSERT_TRUE(r.exitCode == 0); + ASSERT_TRUE(stderrContains(r, "[2048 x i8]")); + ASSERT_TRUE(!stderrContains(r, "[0 x i8]")); + } + + // A repeat literal whose count disagrees with the destination array + // length must be a compile error — the memset/store lowering sizes + // from the count, so letting it through writes past the slot. + static void testArrayRepeatCountMismatchRejected() { + auto r = compileSource("array_repeat_mismatch", R"( +fn main() { + const a: [4]u8 = [0; 0x800]; +} +)"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains( + r, "array repeat count 2048 does not match array type length 4")); + } + + static void testArrayLitTooLongRejected() { + auto r = compileSource("array_lit_too_long", R"( +fn main() { + const a: [2]u8 = [1, 2, 3]; +} +)"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains( + r, "array literal has 3 element(s) but the array type expects 2")); + } + + static void testArrayLitTooShortRejected() { + auto r = compileSource("array_lit_too_short", R"( +fn main() { + const a: [4]u8 = [1, 2]; +} +)"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains( + r, "array literal has 2 element(s) but the array type expects 4")); + } + + static void testFloatArraySizeRejected() { + auto r = compileSource("array_float_size", R"( +fn main() { + var a: [1.5]u8 = [0; 1]; +} +)"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE( + stderrContains(r, "array size must be a non-negative integer")); + } + + static void testHugeArraySizeRejected() { + auto r = compileSource("array_huge_size", R"( +fn main() { + var a: [0x1_0000_0000]u8 = [0; 1]; +} +)"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains(r, "exceeds u32 range")); + } + + // Zig errors `expected 8 array elements; found 0` here; jam's `[]` + // empty literal must hit the same wall instead of leaving the array + // uninitialized. + static void testEmptyLitIntoSizedArrayRejected() { + auto r = compileSource("array_empty_lit", R"( +fn main() { + const a: [8]u8 = []; +} +)"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains( + r, "array literal has 0 element(s) but the array type expects 8")); + } + + // The size token in `[N]T` inherits the FULL number-literal grammar, + // not just "digits the old std::stoul happened to eat": uppercase + // base prefixes, underscores glued to the prefix, and C-style + // leading zeros are rejected with the same diagnostics value + // position produces. + static void testArraySizeLiteralGrammarParity() { + auto upper = compileSource("array_size_upper_base", R"( +fn main() { + var a: [0X800]u8 = [0; 2048]; +} +)"); + ASSERT_TRUE(upper.exitCode != 0); + ASSERT_TRUE(stderrContains(upper, "base prefix must be lowercase")); + + auto glued = compileSource("array_size_underscore_after_base", R"( +fn main() { + var a: [0x_800]u8 = [0; 2048]; +} +)"); + ASSERT_TRUE(glued.exitCode != 0); + ASSERT_TRUE(stderrContains(glued, "underscore not allowed")); + + auto leading = compileSource("array_size_leading_zero", R"( +fn main() { + var a: [07]u8 = [0; 7]; +} +)"); + ASSERT_TRUE(leading.exitCode != 0); + ASSERT_TRUE(stderrContains(leading, "leading zero is not allowed")); + } + + // `[SIZE]u8` with a module const folds at type-resolution time and + // lowers identically to the literal spelling — the IR must carry + // the folded length. Mirrors how a comptime-known length resolves + // before lowering in the reference compiler. + static void testConstArraySizeLowersCorrectly() { + auto r = compileSourceIR("array_const_size_ir", R"( +const SIZE = 0x800; +const Block = struct { + cells: [SIZE]u8, +}; +fn main() { + var b = Block{cells: [0; SIZE]}; + b.cells[0] = 1; +} +)"); + ASSERT_TRUE(r.exitCode == 0); + ASSERT_TRUE(stderrContains(r, "[2048 x i8]")); + ASSERT_TRUE(!stderrContains(r, "[0 x i8]")); + } + + // Undefined names and runtime variables can't fold — both must be + // rejected with the comptime-known diagnostic, not silently sized. + static void testNonComptimeSizeRejected() { + auto undef = compileSource("array_size_undefined", R"( +fn main() { + var a: [BOGUS]u8 = [0; 4]; +} +)"); + ASSERT_TRUE(undef.exitCode != 0); + ASSERT_TRUE( + stderrContains(undef, "array length must be comptime-known")); + + auto runtime = compileSource("array_size_runtime_var", R"( +fn main() { + var n: u32 = 5; + var a: [n]u8 = [0; 5]; +} +)"); + ASSERT_TRUE(runtime.exitCode != 0); + ASSERT_TRUE( + stderrContains(runtime, "array length must be comptime-known")); + } + + static void testNegativeConstSizeRejected() { + auto r = compileSource("array_size_negative_const", R"( +const NEG = 0 - 4; +fn main() { + var a: [NEG]u8 = [0; 4]; +} +)"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE( + stderrContains(r, "array size must be a non-negative integer")); + } + + static void testFloatConstSizeRejected() { + auto r = compileSource("array_size_float_const", R"( +const F = 1.5; +fn main() { + var a: [F]u8 = [0; 1]; +} +)"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE( + stderrContains(r, "array size must be a non-negative integer")); + } + + // The folded length feeds the same literal-length checks as a + // written-out size: a 3-element literal into `[N]u8` with N = 4 + // reports the mismatch with the RESOLVED length. + static void testConstSizeMismatchRejected() { + auto r = compileSource("array_size_const_mismatch", R"( +const N = 4; +fn main() { + const a: [N]u8 = [1, 2, 3]; +} +)"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains( + r, "array literal has 3 element(s) but the array type expects 4")); + } + + // A test binary killed by a signal has no exit status; decoding it + // with WEXITSTATUS alone reads 0 and reports the crashed run as + // "passed" — with the child's unflushed "testing ..." line lost, the + // failure would be completely invisible. The runner must surface the + // signal and return shell-convention 128+sig. + static void testSignalKilledBinaryReported() { + auto r = runTestMode("harness_signal_crash", R"( +tfn crashByWildStore() { + var addr: u64 = 1; + var p: *mut[] u8 = addr as *mut[] u8; + p[0] = 9; +} +)"); + ASSERT_TRUE(r.exitCode > 128); + ASSERT_TRUE(stderrContains(r, "terminated by signal")); + } }; int main() {