From ca7325af4345b38d459d7da0eed7c028419d9383 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Wed, 20 May 2026 11:00:39 +0200 Subject: [PATCH] fix Vec(T) and require StructName for decl --- src/analyzer.cpp | 639 +++++++++++++++++++++ src/analyzer.h | 146 +++++ src/codegen.cpp | 59 +- src/codegen.h | 20 + src/decl.cpp | 71 +++ src/decl.h | 195 +++++++ src/diagnostics.cpp | 5 +- src/diagnostics.h | 12 + src/lexer.cpp | 88 +-- src/lexer.h | 9 +- src/main.cpp | 354 +++++------- src/module_resolver.cpp | 3 +- src/number_literal.cpp | 2 +- src/number_literal.h | 3 +- src/parser.cpp | 279 +++++++-- src/parser.h | 18 +- src/token.h | 26 +- tests/cpp/test_analyzer.cpp | 313 ++++++++++ tests/cpp/test_codegen_errors.cpp | 20 +- tests/cpp/test_decl_table.cpp | 154 +++++ tests/cpp/test_diagnostics.cpp | 205 ++++++- tests/cpp/test_init_analysis.cpp | 15 +- tests/unit/test_abi_extern_mixed.jam | 4 +- tests/unit/test_abi_large.jam | 8 +- tests/unit/test_abi_sret.jam | 6 +- tests/unit/test_array.jam | 6 +- tests/unit/test_drops.jam | 8 +- tests/unit/test_drops_loops.jam | 8 +- tests/unit/test_drops_mangling.jam | 8 +- tests/unit/test_drops_scoped.jam | 14 +- tests/unit/test_generics_basic.jam | 44 +- tests/unit/test_indexed_field_chain.jam | 18 +- tests/unit/test_init.jam | 2 +- tests/unit/test_instance_method_call.jam | 8 +- tests/unit/test_match_scrutinee_shapes.jam | 2 +- tests/unit/test_modes_escape.jam | 2 +- tests/unit/test_modes_exclusivity.jam | 2 +- tests/unit/test_pointer.jam | 2 +- tests/unit/test_struct.jam | 24 +- tests/unit/test_struct_field_index.jam | 6 +- tests/unit/test_struct_methods.jam | 6 +- tests/unit/test_typecall_method.jam | 2 +- tests/unit/test_union.jam | 22 +- 43 files changed, 2402 insertions(+), 436 deletions(-) create mode 100644 src/analyzer.cpp create mode 100644 src/analyzer.h create mode 100644 src/decl.cpp create mode 100644 src/decl.h create mode 100644 tests/cpp/test_analyzer.cpp create mode 100644 tests/cpp/test_decl_table.cpp diff --git a/src/analyzer.cpp b/src/analyzer.cpp new file mode 100644 index 0000000..3aec210 --- /dev/null +++ b/src/analyzer.cpp @@ -0,0 +1,639 @@ +/* + * Copyright (c) 2026-present Raphael Amorim + * + * This file is part of jam. + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions. + */ + +#include "analyzer.h" + +#include "abi.h" +#include "ast.h" +#include "codegen.h" + +#include +#include + +namespace jam { + +Analyzer::Analyzer(JamCodegenContext &ctx, DeclTable &decls) + : ctx_(ctx), decls_(decls) {} + +SrcLoc Analyzer::locOf(const Decl &d) const { + SrcLoc loc; + loc.file = d.file.empty() ? ctx_.currentFile() : d.file; + loc.line = d.line; + return loc; +} + +void Analyzer::pushCycleError(DeclIndex repeated) { + const Decl &repeatedDecl = decls_.get(repeated); + std::ostringstream chain; + // The cycle is the suffix of analysisStack_ that begins with the + // first occurrence of `repeated`. Walk from there to the end so + // the user reads the loop in source order: A → B → A. + auto first = + std::find(analysisStack_.begin(), analysisStack_.end(), repeated); + chain << "dependency loop detected: "; + for (auto it = first; it != analysisStack_.end(); ++it) { + if (it != first) chain << " → "; + chain << "`" << decls_.get(*it).name << "`"; + } + chain << " → `" << repeatedDecl.name << "`"; + // The chain itself is in the message, so the reference trace + // would just duplicate it. Skip the trace here; other diagnostics + // that don't print the chain get it via emitErrorWithRefTrace. + ctx_.diagnostics().error(locOf(repeatedDecl), chain.str()); +} + +std::vector +Analyzer::buildReferenceTrace(bool excludeTop) const { + std::vector trace; + if (analysisStack_.empty()) return trace; + // Walk the stack from outermost to innermost so the user reads + // "referenced by `outer` ... referenced by `middle`" in causal + // order. `excludeTop` drops the innermost frame, used when the + // error message already names that decl. + std::size_t end = analysisStack_.size(); + if (excludeTop && end > 0) --end; + for (std::size_t i = 0; i < end; ++i) { + const Decl &d = decls_.get(analysisStack_[i]); + Diagnostic::Trace t; + t.kind = Diagnostic::Trace::Kind::Reference; + t.decl = d.name; + t.loc = locOf(d); + trace.push_back(std::move(t)); + } + return trace; +} + +void Analyzer::emitErrorWithRefTrace(SrcLoc loc, std::string message, + bool excludeTop) { + auto trace = buildReferenceTrace(excludeTop); + if (trace.empty()) { + ctx_.diagnostics().error(std::move(loc), std::move(message)); + return; + } + ctx_.diagnostics().errorWithTrace(std::move(loc), std::move(message), + std::move(trace)); +} + +DeclValue Analyzer::ensureDeclAnalyzed(DeclIndex idx) { + if (idx == kNoDecl) return {}; + Decl &d = decls_.get(idx); + + switch (d.analysis) { + case DeclAnalysis::Complete: + return d.value; + case DeclAnalysis::InProgress: + pushCycleError(idx); + // Don't mark this decl as failed — the cycle is the *caller's* + // problem too. Returning a poison value lets the caller decide + // whether to bail. The next ensureDeclAnalyzed for this same + // idx (after the stack unwinds) will hit `Complete` cleanly. + return {}; + case DeclAnalysis::AnalysisFailure: + case DeclAnalysis::DependencyFailure: + return {}; + case DeclAnalysis::Unreferenced: + break; + } + + // Move to InProgress, dispatch, then to Complete (or failure). + d.analysis = DeclAnalysis::InProgress; + analysisStack_.push_back(idx); + DeclValue value = analyzeDecl(idx); + analysisStack_.pop_back(); + + // Re-fetch — analyzeDecl may have appended new decls and + // invalidated our reference. Decl& is stable across creates only + // when std::vector doesn't reallocate; safer to look up again. + Decl &again = decls_.get(idx); + if (again.analysis == DeclAnalysis::InProgress) { + // Branch didn't set a terminal state: default to Complete with + // whatever value was returned. (A branch that pushed a cycle + // error returns None but stays InProgress so the unwind path + // here marks it Complete-with-None — that's a *resolved + // state*, callers see "no value" but no infinite recursion on + // retry.) + again.analysis = DeclAnalysis::Complete; + again.value = value; + } + return again.value; +} + +bool Analyzer::resolveTypeFieldsStruct(DeclIndex idx) { + if (idx == kNoDecl) return false; + Decl &d = decls_.get(idx); + if (d.kind != DeclKind::Struct) return false; + + switch (d.structStatus) { + case StructStatus::HaveFieldTypes: + case StructStatus::HaveLayout: + return true; + case StructStatus::FieldTypesWIP: { + // Distinct error from the Decl-level "dependency loop" — this + // is specifically a struct whose own fields reference itself + // (via something that wasn't a pointer or slice indirection). + // Build a Reference trace from the struct-fill stack so the + // user sees the chain `B referenced by A` when a multi-step + // cycle hits (the message itself only names the cycle's + // closing decl). + std::vector trace; + for (DeclIndex anc : structFillStack_) { + if (anc == idx) continue; // skip the closing decl + const Decl &ad = decls_.get(anc); + Diagnostic::Trace t; + t.kind = Diagnostic::Trace::Kind::Reference; + t.decl = ad.name; + t.loc = locOf(ad); + trace.push_back(std::move(t)); + } + if (trace.empty()) { + ctx_.diagnostics().error(locOf(d), "struct `" + d.name + + "` depends on itself"); + } else { + ctx_.diagnostics().errorWithTrace( + locOf(d), "struct `" + d.name + "` depends on itself", + std::move(trace)); + } + return false; + } + case StructStatus::LayoutWIP: + // Layout depends on field types, so if we're here the field + // types are already known; treat as success. + return true; + case StructStatus::None: + break; + } + + d.structStatus = StructStatus::FieldTypesWIP; + structFillStack_.push_back(idx); + + // Walk every field and materialise its LLVM type. Each call to + // `getLLVMType` is a chance to re-enter the analyzer (when the + // field's TypeIdx is a Named struct whose body isn't yet set, or + // a GenericCall): that re-entry hits the chokepoint and either + // detects the FieldTypesWIP cycle (self-ref) or recurses into a + // different struct's resolution. The previous eager + // `fillStructBodies` in main.cpp did this inline; routing through + // the analyzer is what makes the cycle detection structural. + if (d.structAst == nullptr) { + // No AST attached — Decl was created with a name but no + // body. Treat as already-resolved so callers don't loop. + d.structStatus = StructStatus::HaveFieldTypes; + structFillStack_.pop_back(); + return true; + } + const auto &fields = d.structAst->Fields; + std::vector fieldTypes; + fieldTypes.reserve(fields.size()); + bool fieldFillFailed = false; + for (const auto &f : fields) { + fieldTypes.push_back(ctx_.getLLVMType(f.second)); + // Cycle detection sits at the field position, not in + // getLLVMType — because getLLVMType for `*mut S` also recurses + // into S, and a pointer field does NOT make S sized-dependent + // on itself. By peeking at the TypeKey here we only trigger + // the matching ensure*Body when the field is a *direct* named + // type (TypeKind::Named/Struct/Enum/Union, not PtrSingle/ + // PtrMany/Slice/Array), so linked-list idioms keep working + // while `S { x: S }` (or struct→union→struct chains) correctly + // trips the WIP cycle check. + const TypeKey &fk = ctx_.getTypePool().get(f.second); + if (fk.kind == TypeKind::Named || fk.kind == TypeKind::Struct || + fk.kind == TypeKind::Enum || fk.kind == TypeKind::Union) { + const std::string &fieldName = + ctx_.getStringPool().get(static_cast(fk.a)); + bool nestedOk = true; + if (ctx_.getStruct(fieldName)) { + nestedOk = ensureStructBody(fieldName); + } else if (ctx_.getEnum(fieldName)) { + nestedOk = ensureEnumBody(fieldName); + } else if (ctx_.getUnion(fieldName)) { + nestedOk = ensureUnionBody(fieldName); + } + if (!nestedOk) fieldFillFailed = true; + } + } + const auto *info = ctx_.getStruct(d.name); + if (info != nullptr && info->type != nullptr) { + if (fieldFillFailed) { + // Downstream pushed a diagnostic; leave a single-i8 body + // so callers can still query a size without UB. + JamTypeRef stub = ctx_.getInt8Type(); + JamLLVMStructSetBody(info->type, &stub, 1, false); + } else { + JamLLVMStructSetBody(info->type, fieldTypes.data(), + static_cast(fieldTypes.size()), + false); + } + } + d.structStatus = StructStatus::HaveFieldTypes; + structFillStack_.pop_back(); + return !fieldFillFailed; +} + +bool Analyzer::ensureStructBody(const std::string &name) { + // Jam permits a function and a struct to share a name (the type + // shows up in type position, the function in call position). + // Use the kind-aware finder so we land on the Struct decl even + // when a same-name function was registered first. + DeclIndex idx = decls_.findByNameAndKind(name, DeclKind::Struct); + if (idx == kNoDecl) return false; + return resolveTypeFieldsStruct(idx); +} + +bool Analyzer::ensureEnumBody(const std::string &name) { + DeclIndex idx = decls_.findByNameAndKind(name, DeclKind::Enum); + if (idx == kNoDecl) return false; + return resolveTypeFieldsEnum(idx); +} + +bool Analyzer::ensureUnionBody(const std::string &name) { + DeclIndex idx = decls_.findByNameAndKind(name, DeclKind::Union); + if (idx == kNoDecl) return false; + return resolveTypeFieldsUnion(idx); +} + +DeclValue Analyzer::resolveDecl(const std::string &name) { + DeclIndex idx = decls_.findByName(name); + if (idx == kNoDecl) return {}; + return ensureDeclAnalyzed(idx); +} + +bool Analyzer::resolveTypeFieldsEnum(DeclIndex idx) { + if (idx == kNoDecl) return false; + Decl &d = decls_.get(idx); + if (d.kind != DeclKind::Enum) return false; + + switch (d.enumStatus) { + case EnumStatus::HaveBody: + return true; + case EnumStatus::BodyWIP: { + // Self-cycle: a payload type references the enum itself. + std::vector trace; + for (DeclIndex anc : enumFillStack_) { + if (anc == idx) continue; + const Decl &ad = decls_.get(anc); + Diagnostic::Trace t; + t.kind = Diagnostic::Trace::Kind::Reference; + t.decl = ad.name; + t.loc = locOf(ad); + trace.push_back(std::move(t)); + } + if (trace.empty()) { + ctx_.diagnostics().error(locOf(d), + "enum `" + d.name + "` depends on itself"); + } else { + ctx_.diagnostics().errorWithTrace( + locOf(d), "enum `" + d.name + "` depends on itself", + std::move(trace)); + } + return false; + } + case EnumStatus::None: + break; + } + + // Layout matches main.cpp's prior fillEnumBodies: only payloaded + // enums need a struct body; unit-only enums lower to plain i8 and + // have no LLVM struct to set. The math computes per-variant size / + // alignment, picks an alignDriver scalar matching the strictest + // variant's alignment, and pads the trailing slot up to the worst- + // case payload size so an array of enums tiles correctly. + d.enumStatus = EnumStatus::BodyWIP; + enumFillStack_.push_back(idx); + + const auto *info = ctx_.getEnum(d.name); + if (info == nullptr || !info->hasPayloadVariant) { + d.enumStatus = EnumStatus::HaveBody; + enumFillStack_.pop_back(); + return true; + } + + uint64_t maxSize = 0; + uint64_t maxAlign = 1; + bool payloadFillFailed = false; + for (const auto &v : info->variants) { + if (payloadFillFailed) break; + uint64_t off = 0; + uint64_t varAlign = 1; + for (TypeIdx t : v.payloadTypes) { + // Trigger nested body fills for direct named-type + // payloads so typeSize/typeAlign see a fully-laid-out + // LLVM type. Pointer payloads (PtrSingle/PtrMany) skip + // this — the pointee size doesn't depend on the + // pointee's body, so they don't need (and shouldn't + // trigger) the WIP cycle check. + const TypeKey &tk = ctx_.getTypePool().get(t); + if (tk.kind == TypeKind::Named || tk.kind == TypeKind::Struct || + tk.kind == TypeKind::Enum || tk.kind == TypeKind::Union) { + const std::string &payloadName = + ctx_.getStringPool().get(static_cast(tk.a)); + bool nestedOk = true; + if (ctx_.getStruct(payloadName)) { + nestedOk = ensureStructBody(payloadName); + } else if (ctx_.getEnum(payloadName)) { + nestedOk = ensureEnumBody(payloadName); + } else if (ctx_.getUnion(payloadName)) { + nestedOk = ensureUnionBody(payloadName); + } + if (!nestedOk) { + payloadFillFailed = true; + break; + } + } + uint64_t s = ctx_.typeSize(t); + uint64_t a = ctx_.typeAlign(t); + off = (off + a - 1) / a * a; + off += s; + if (a > varAlign) varAlign = a; + } + if (varAlign > 1) { off = (off + varAlign - 1) / varAlign * varAlign; } + if (off > maxSize) maxSize = off; + if (varAlign > maxAlign) maxAlign = varAlign; + } + if (payloadFillFailed) { + // Stub body: {i8 tag, i8 payload}. Downstream queries see a + // well-formed (if useless) LLVM type so the build doesn't + // crash before the diagnostics get printed. + JamTypeRef body[2] = {ctx_.getInt8Type(), ctx_.getInt8Type()}; + JamLLVMStructSetBody(info->type, body, 2, false); + ctx_.setEnumLLVMType(d.name, info->type, 0, 1, true); + d.enumStatus = EnumStatus::HaveBody; + enumFillStack_.pop_back(); + return false; + } + + JamTypeRef alignDriver; + uint64_t alignDriverSize; + switch (maxAlign) { + case 1: + alignDriver = ctx_.getInt8Type(); + alignDriverSize = 1; + break; + case 2: + alignDriver = ctx_.getInt16Type(); + alignDriverSize = 2; + break; + case 4: + alignDriver = ctx_.getInt32Type(); + alignDriverSize = 4; + break; + case 8: + alignDriver = ctx_.getInt64Type(); + alignDriverSize = 8; + break; + default: + ctx_.diagnostics().error( + locOf(d), + "enum `" + d.name + + "` requires alignment > 8, which is not yet supported"); + d.enumStatus = EnumStatus::HaveBody; + enumFillStack_.pop_back(); + return false; + } + + uint64_t paddedSize = (maxSize + maxAlign - 1) / maxAlign * maxAlign; + uint64_t extraBytes = + paddedSize > alignDriverSize ? paddedSize - alignDriverSize : 0; + + std::vector bodyTypes; + bodyTypes.push_back(ctx_.getInt8Type()); // tag + bodyTypes.push_back(alignDriver); + if (extraBytes > 0) { + bodyTypes.push_back(JamLLVMArrayType( + ctx_.getInt8Type(), static_cast(extraBytes))); + } + + JamLLVMStructSetBody(info->type, bodyTypes.data(), + static_cast(bodyTypes.size()), false); + ctx_.setEnumLLVMType(d.name, info->type, maxSize, maxAlign, true); + + d.enumStatus = EnumStatus::HaveBody; + enumFillStack_.pop_back(); + return true; +} + +bool Analyzer::resolveTypeFieldsUnion(DeclIndex idx) { + if (idx == kNoDecl) return false; + Decl &d = decls_.get(idx); + if (d.kind != DeclKind::Union) return false; + + switch (d.unionStatus) { + case UnionStatus::HaveBody: + return true; + case UnionStatus::BodyWIP: { + std::vector trace; + for (DeclIndex anc : unionFillStack_) { + if (anc == idx) continue; + const Decl &ad = decls_.get(anc); + Diagnostic::Trace t; + t.kind = Diagnostic::Trace::Kind::Reference; + t.decl = ad.name; + t.loc = locOf(ad); + trace.push_back(std::move(t)); + } + if (trace.empty()) { + ctx_.diagnostics().error(locOf(d), "union `" + d.name + + "` depends on itself"); + } else { + ctx_.diagnostics().errorWithTrace( + locOf(d), "union `" + d.name + "` depends on itself", + std::move(trace)); + } + return false; + } + case UnionStatus::None: + break; + } + + // Layout: { alignedField, [paddingBytes x i8] } where alignedField + // is the field with the strictest alignment. Padding pads up to the + // largest field's size so the union has the right size and + // alignment for any stored variant. + d.unionStatus = UnionStatus::BodyWIP; + unionFillStack_.push_back(idx); + + const auto *info = ctx_.getUnion(d.name); + if (info == nullptr || info->fields.empty()) { + ctx_.diagnostics().error( + locOf(d), "union `" + d.name + "` must have at least one field"); + d.unionStatus = UnionStatus::HaveBody; + unionFillStack_.pop_back(); + return false; + } + + uint64_t maxSize = 0; + uint64_t maxAlign = 1; + std::size_t alignFieldIdx = 0; + bool fieldFillFailed = false; + for (std::size_t i = 0; i < info->fields.size(); ++i) { + TypeIdx t = info->fields[i].second; + const TypeKey &tk = ctx_.getTypePool().get(t); + if (tk.kind == TypeKind::Named || tk.kind == TypeKind::Struct || + tk.kind == TypeKind::Enum || tk.kind == TypeKind::Union) { + const std::string &fieldName = + ctx_.getStringPool().get(static_cast(tk.a)); + bool nestedOk = true; + if (ctx_.getStruct(fieldName)) { + nestedOk = ensureStructBody(fieldName); + } else if (ctx_.getEnum(fieldName)) { + nestedOk = ensureEnumBody(fieldName); + } else if (ctx_.getUnion(fieldName)) { + nestedOk = ensureUnionBody(fieldName); + } + if (!nestedOk) { + // A downstream cycle or failure already pushed a + // diagnostic. Calling typeSize/typeAlign on a still- + // empty LLVM struct would crash, so bail with a + // stub body for this union. + fieldFillFailed = true; + break; + } + } + uint64_t s = ctx_.typeSize(t); + uint64_t a = ctx_.typeAlign(t); + if (s > maxSize) maxSize = s; + if (a > maxAlign) { + maxAlign = a; + alignFieldIdx = i; + } + } + if (fieldFillFailed) { + // Emit a single-i8 body so downstream code that asks for the + // union's size doesn't see undefined behavior. The diagnostic + // already exists from the recursive call. + JamTypeRef i8 = ctx_.getInt8Type(); + JamLLVMStructSetBody(info->type, &i8, 1, false); + d.unionStatus = UnionStatus::HaveBody; + unionFillStack_.pop_back(); + return false; + } + uint64_t allocSize = (maxSize + maxAlign - 1) / maxAlign * maxAlign; + JamTypeRef alignedTy = ctx_.getLLVMType(info->fields[alignFieldIdx].second); + uint64_t alignedSz = ctx_.typeSize(info->fields[alignFieldIdx].second); + uint64_t paddingBytes = allocSize > alignedSz ? allocSize - alignedSz : 0; + + std::vector bodyTypes; + bodyTypes.push_back(alignedTy); + if (paddingBytes > 0) { + bodyTypes.push_back(JamLLVMArrayType( + ctx_.getInt8Type(), static_cast(paddingBytes))); + } + JamLLVMStructSetBody(info->type, bodyTypes.data(), + static_cast(bodyTypes.size()), false); + + d.unionStatus = UnionStatus::HaveBody; + unionFillStack_.pop_back(); + return true; +} + +DeclValue Analyzer::analyzeDecl(DeclIndex idx) { + Decl &d = decls_.get(idx); + switch (d.kind) { + case DeclKind::Function: + return analyzeFunction(idx); + case DeclKind::Struct: + return analyzeStruct(idx); + case DeclKind::Enum: + return analyzeEnum(idx); + case DeclKind::Union: + return analyzeUnion(idx); + case DeclKind::Const: + return analyzeConst(idx); + case DeclKind::TypeAlias: + // Same shape as Const for now; the const-or-alias decision is + // made inside analyzeConst by inspecting the InitExpr. + return analyzeConst(idx); + case DeclKind::Invalid: + return {}; + } + return {}; +} + +DeclValue Analyzer::analyzeStruct(DeclIndex idx) { + Decl &d = decls_.get(idx); + // Body fill is part of the struct's own analysis — calling + // resolveTypeFieldsStruct here means ensureDeclAnalyzed(structIdx) + // is now the single entry point that both proves the cycle-free + // status AND materialises the LLVM body. If the body walk hits + // FieldTypesWIP it already pushed an error; the Decl still ends + // up Complete (with the placeholder Named TypeIdx) so callers + // receive a usable type and the error appears once. + resolveTypeFieldsStruct(idx); + DeclValue v; + v.kind = DeclValue::Kind::Type; + v.type = + ctx_.getTypePool().internNamed(ctx_.getStringPool().intern(d.name)); + return v; +} + +DeclValue Analyzer::analyzeEnum(DeclIndex idx) { + resolveTypeFieldsEnum(idx); + Decl &d = decls_.get(idx); + DeclValue v; + v.kind = DeclValue::Kind::Type; + v.type = + ctx_.getTypePool().internNamed(ctx_.getStringPool().intern(d.name)); + return v; +} + +DeclValue Analyzer::analyzeUnion(DeclIndex idx) { + resolveTypeFieldsUnion(idx); + Decl &d = decls_.get(idx); + DeclValue v; + v.kind = DeclValue::Kind::Type; + v.type = + ctx_.getTypePool().internNamed(ctx_.getStringPool().intern(d.name)); + return v; +} + +DeclValue Analyzer::analyzeFunction(DeclIndex idx) { + Decl &d = decls_.get(idx); + // Cache the ABI signature so downstream codegen + JIR + call sites + // can ask one source for "how does this fn get lowered?" instead + // of re-running classifyParam/classifyReturn at each site. Generic + // functions skip the cache: each instantiation has its own + // substituted TypeIdxs that arrive only at call time, so caching + // at the decl would never hit. + if (d.fnAst != nullptr && !d.fnAst->isGeneric() && !d.signature.computed) { + d.signature.params.reserve(d.fnAst->Args.size()); + for (const auto &p : d.fnAst->Args) { + d.signature.params.push_back( + jam::abi::classifyParam(p.Mode, p.Type, ctx_)); + } + d.signature.returnAbi = + jam::abi::classifyReturn(d.fnAst->ReturnType, ctx_); + d.signature.computed = true; + } + DeclValue v; + v.kind = DeclValue::Kind::Function; + v.function = d.fnAst; + return v; +} + +DeclValue Analyzer::analyzeConst(DeclIndex idx) { + Decl &d = decls_.get(idx); + // `AliasedType` is set by `registerConsts` in main.cpp when it + // detects that the const's RHS is a type-resolving expression + // (e.g. `const Box = Vec(i32)` → AliasedType = GenericCall ty). + // Reading that field here lets the analyzer surface the alias + // as a Type-kind DeclValue, so future call sites that ask the + // analyzer "what is X?" get a TypeIdx back instead of None. + // + // Consts that stay as values (e.g. `const PI = 3.14`) keep + // AliasedType == kNoType — the analyzer returns a None-valued + // DeclValue and the existing JamCodegenContext::registerModuleConst + // path handles the runtime value. + if (d.constAst != nullptr && d.constAst->AliasedType != kNoType) { + DeclValue v; + v.kind = DeclValue::Kind::Type; + v.type = d.constAst->AliasedType; + return v; + } + return {}; +} + +} // namespace jam diff --git a/src/analyzer.h b/src/analyzer.h new file mode 100644 index 0000000..83602fe --- /dev/null +++ b/src/analyzer.h @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2026-present Raphael Amorim + * + * This file is part of jam. + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions. + */ + +#ifndef JAM_ANALYZER_H +#define JAM_ANALYZER_H + +#include "decl.h" +#include "diagnostics.h" + +#include + +class JamCodegenContext; + +namespace jam { + +// Demand-driven decl analysis. Replaces the explicit phase pipeline +// that used to live in main.cpp::compileAndRun. Two state machines +// stack on top of the DeclTable: +// +// * `Decl.analysis` (Unreferenced/InProgress/Complete/...) — the +// chokepoint at `ensureDeclAnalyzed`. A decl whose value is +// currently being computed is `InProgress`; re-entering it from +// anywhere is a dependency loop. +// +// * `Struct.status` (None/FieldTypesWIP/HaveFieldTypes/...) — a +// separate lifecycle for *struct bodies*. A struct Decl can be +// `Complete` (value = a NamedType TypeIdx) long before its +// fields are materialized; `resolveTypeFieldsStruct` is what +// forces the field types when something — `getLLVMType`, +// `@sizeOf`, a field access — actually needs them. +// +// The split lets `struct Container { x: Vec(i32) }` work even when +// `Vec`'s Decl hasn't been analysed yet: Container is `Complete` as +// soon as its NamedType is interned; the demand for `x.ty` later +// pulls in `Vec` recursively without any fixed phase ordering. +class Analyzer { + public: + Analyzer(JamCodegenContext &ctx, DeclTable &decls); + + // Ensure `idx`'s value has been computed. Returns the value (a + // type, function, etc.); on cycle / failure returns a None-valued + // DeclValue and the Decl's analysis state reflects the failure. + // Diagnostics are pushed to `ctx.diagnostics()`. + // + // Idempotent: a Complete decl returns the cached value with no + // work. An InProgress decl triggers the cycle-detector. + DeclValue ensureDeclAnalyzed(DeclIndex idx); + + // Force struct-field types to be materialized. Distinct from + // ensureDeclAnalyzed: a struct's Decl can be Complete (its value + // is the NamedType) while its body is still uncomputed. Returns + // false on `FieldTypesWIP` re-entry (struct depends on itself) + // or any field-eval failure. + bool resolveTypeFieldsStruct(DeclIndex idx); + + // Same shape as resolveTypeFieldsStruct, but for enums and + // unions. Computes the LLVM body (variant layout / tagged-union + // padding) and stamps EnumStatus::HaveBody / UnionStatus::HaveBody + // when done. Re-entry into a BodyWIP decl is a self-cycle. + bool resolveTypeFieldsEnum(DeclIndex idx); + bool resolveTypeFieldsUnion(DeclIndex idx); + + // Look up a struct/enum/union by source-level name and route the + // body-fill through the matching `resolveTypeFields*`. Used by + // JamCodegenContext::getLLVMType so any code path that asks for + // an LLVM type whose body hasn't been filled triggers demand- + // driven materialisation + cycle detection. Returns true if the + // body is set after the call. + bool ensureStructBody(const std::string &name); + bool ensureEnumBody(const std::string &name); + bool ensureUnionBody(const std::string &name); + + // Demand-driven name lookup: find the decl, force its analysis, + // and return the resolved DeclValue. Returns a None-kind value if + // the name doesn't exist in the DeclTable. The eager Step E loop + // pre-analyzes all type-shaped decls, so today this is mostly a + // chokepoint — but any future flip to lazy decl analysis (e.g. + // per-module or incremental rebuilds) keeps astgen callers + // working without ordering assumptions. + DeclValue resolveDecl(const std::string &name); + + // Public read of the current analysis stack — used by Step 5's + // reference-trace formatter and by tests. + const std::vector &analysisStack() const { + return analysisStack_; + } + + private: + // Dispatch on Decl::kind. Each branch writes the resolved + // DeclValue back into the Decl and returns it. Errors are pushed + // to diagnostics; the branch returns a None-valued DeclValue and + // the Decl's analysis state is set to AnalysisFailure / + // DependencyFailure. Branches receive the DeclIndex (not just + // Decl&) so they can call back into ensureDeclAnalyzed for + // dependencies and into resolveTypeFieldsStruct without a + // secondary name lookup. + DeclValue analyzeDecl(DeclIndex idx); + DeclValue analyzeStruct(DeclIndex idx); + DeclValue analyzeEnum(DeclIndex idx); + DeclValue analyzeUnion(DeclIndex idx); + DeclValue analyzeFunction(DeclIndex idx); + DeclValue analyzeConst(DeclIndex idx); + + // Push a "dependency loop detected: A → B → A" diagnostic, using + // the current analysis stack as the chain. + void pushCycleError(DeclIndex repeated); + + // Build a reference trace from the analysis stack — every ancestor + // of the current decl, oldest-first ("referenced by `outer` + // referenced by `middle` …"). `excludeTop` skips the topmost stack + // entry when it would just restate the diagnostic's own location + // (e.g. a self-cycle error already names the struct in its message). + std::vector + buildReferenceTrace(bool excludeTop = false) const; + + // Push an error with the reference trace pre-attached. + void emitErrorWithRefTrace(SrcLoc loc, std::string message, + bool excludeTop = false); + + // SrcLoc for the Decl, used by diagnostic helpers. + SrcLoc locOf(const Decl &d) const; + + JamCodegenContext &ctx_; + DeclTable &decls_; + // Stack of currently-analyzing Decl indices. Pushed on entry to + // ensureDeclAnalyzed (when not yet Complete), popped on exit. A + // decl already on the stack when asked again is a cycle. + std::vector analysisStack_; + // Parallel stack for struct-body fills. resolveTypeFieldsStruct + // has its own lifecycle (StructStatus::FieldTypesWIP) that + // doesn't go through ensureDeclAnalyzed, so analysisStack_ is + // empty for struct-field cycles. This stack captures the + // "ancestor structs whose field walks led to here" so the cycle + // error can show a chain like `B referenced by A`. + std::vector structFillStack_; + std::vector enumFillStack_; + std::vector unionFillStack_; +}; + +} // namespace jam + +#endif // JAM_ANALYZER_H diff --git a/src/codegen.cpp b/src/codegen.cpp index 6f381e9..10b4511 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -7,6 +7,7 @@ #include "codegen.h" +#include "analyzer.h" #include "ast.h" #include "astgen.h" #include "jir_codegen.h" @@ -27,6 +28,17 @@ JamCodegenContext::~JamCodegenContext() { JamLLVMDisposeContext(ctx); } +jam::Analyzer &JamCodegenContext::analyzer() const { + if (!analyzer_) { + // Lazy ctor — needs a fully-constructed JamCodegenContext to + // reference for diagnostics / file / type pool. The DeclTable + // is a sibling member, so its address is stable. + analyzer_ = std::make_unique( + const_cast(*this), declTable_); + } + return *analyzer_; +} + // Parse a type-syntax string into the canonical TypeIdx (recursive). The // parser will eventually produce TypeIdx directly and this function will // only be called by legacy callers that still hold strings. @@ -724,13 +736,52 @@ TypeIdx substituteType(TypeIdx ty, } // namespace TypeIdx JamCodegenContext::resolveGenericCall(TypeIdx callTy) const { - auto cached = genericResolutions_.find(callTy); + // Apply the active substitution context to the GenericCall's args + // before resolving. The same `Inner(T)` TypeIdx appears in every + // instantiation of an outer generic that mentions it — Wrap(i32) + // must see `[i32]`, Wrap(f32) must see `[f32]`. Without this + // substitution the args stay as the placeholder Named("T") and + // the inner instantiation either resolves to a meaningless + // Inner__T or loops because the placeholder never collapses. + // + // The cache key is the *substituted* GenericCall TypeIdx so two + // outer substitutions don't conflate. + const TypeKey &k0 = typePool.get(callTy); + const auto &rawArgs = typePool.genericArgsAt(k0.b); + TypeIdx effectiveTy = callTy; + if (!currentSubst_.empty()) { + std::vector substArgs; + substArgs.reserve(rawArgs.size()); + bool anyChanged = false; + for (TypeIdx a : rawArgs) { + TypeIdx s = substituteType(a, currentSubst_, typePool, stringPool); + if (s != a) anyChanged = true; + substArgs.push_back(s); + } + if (anyChanged) { + effectiveTy = typePool.internGenericCall( + static_cast(k0.a), std::move(substArgs)); + } + } + + auto cached = genericResolutions_.find(effectiveTy); if (cached != genericResolutions_.end()) return cached->second; - const TypeKey &k = typePool.get(callTy); + const TypeKey &k = typePool.get(effectiveTy); const std::string &calleeName = stringPool.get(static_cast(k.a)); const auto &args = typePool.genericArgsAt(k.b); + // Demand-driven: consult the Analyzer first so cycle detection + // kicks in and any cross-decl dependency the user introduced is + // recorded. The analyzer's stub for Function decls just confirms + // the FunctionAST is reachable — the heavy lifting (resolving + // args, instantiating the struct) stays here. Step 4 will hoist + // the instantiation logic into the analyzer itself. + jam::DeclIndex calleeDecl = declTable_.findByName(calleeName); + if (calleeDecl != jam::kNoDecl) { + analyzer().ensureDeclAnalyzed(calleeDecl); + } + // Identity-based lookup: namespace-qualified and bare callees both // resolve to the same FunctionAST (main.cpp registers both forms). // Look it up by whichever name the caller used. @@ -752,7 +803,7 @@ TypeIdx JamCodegenContext::resolveGenericCall(TypeIdx callTy) const { GenericInstanceKey idKey{generic, std::vector(args)}; auto idCached = genericInstances_.find(idKey); if (idCached != genericInstances_.end()) { - genericResolutions_[callTy] = idCached->second; + genericResolutions_[effectiveTy] = idCached->second; return idCached->second; } if (args.size() != generic->Args.size()) { @@ -825,7 +876,7 @@ TypeIdx JamCodegenContext::resolveGenericCall(TypeIdx callTy) const { } genericInstances_[idKey] = result; - genericResolutions_[callTy] = result; + genericResolutions_[effectiveTy] = result; return result; } diff --git a/src/codegen.h b/src/codegen.h index f438410..33a7253 100644 --- a/src/codegen.h +++ b/src/codegen.h @@ -9,6 +9,7 @@ #define CODEGEN_H #include "ast_flat.h" +#include "decl.h" #include "diagnostics.h" #include "drop_registry.h" #include "jam_llvm.h" @@ -25,6 +26,10 @@ class FunctionAST; class StructDeclAST; class EnumDeclAST; +namespace jam { +class Analyzer; +} + class JamCodegenContext { public: JamCodegenContext(const char *moduleName); @@ -198,6 +203,15 @@ class JamCodegenContext { // note: in instantiation of `Pair(Vec(NoDefault), i32)` // when an error surfaces deep inside a monomorphisation. std::vector &refTrace() const { return refTrace_; } + + // Demand-driven decl analysis. The DeclTable is populated by + // the driver (main.cpp) right after parse + module resolution; + // the Analyzer is the chokepoint every getLLVMType / generic- + // instantiation path consults so cycles are caught structurally + // and module-ordering doesn't matter. + jam::DeclTable &declTable() const { return declTable_; } + jam::Analyzer &analyzer() const; + // look up a drop method for an instantiated struct // (e.g. Box__i32). Falls back to the pre-built drop registry. // Returns nullptr if the struct has no drop method. @@ -241,6 +255,12 @@ class JamCodegenContext { std::string currentFile_; mutable std::vector refTrace_; + // Decl table + lazy analyzer. The table is populated by the + // driver post-parse; the analyzer is materialised on first + // `analyzer()` call so the JamCodegenContext ctor stays trivial. + mutable jam::DeclTable declTable_; + mutable std::unique_ptr analyzer_; + // callsite ABI: when codegen for a call expression needs to know // the callee's parameter modes (e.g. to decide whether to auto-take // the address of an arg for a large `let` aggregate), it looks up diff --git a/src/decl.cpp b/src/decl.cpp new file mode 100644 index 0000000..e365bcc --- /dev/null +++ b/src/decl.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026-present Raphael Amorim + * + * This file is part of jam. + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions. + */ + +#include "decl.h" + +#include + +namespace jam { + +DeclTable::DeclTable() { + // Slot 0 = sentinel. kNoDecl reads as Invalid with no fields set. + decls_.emplace_back(); +} + +DeclIndex DeclTable::create(DeclKind kind, std::string name) { + DeclIndex idx = static_cast(decls_.size()); + Decl d; + d.kind = kind; + d.name = name; + decls_.push_back(std::move(d)); + // First registration wins. Later duplicate-name decls (e.g. an + // imported pub fn with the same bare name as a main-module fn) are + // reachable only via qualified-name aliasing, which lives in the + // codegen context's separate alias / handle tables. + byName_.emplace(std::move(name), idx); + return idx; +} + +Decl &DeclTable::get(DeclIndex idx) { return decls_[idx]; } +const Decl &DeclTable::get(DeclIndex idx) const { return decls_[idx]; } + +DeclIndex DeclTable::findByName(const std::string &name) const { + auto it = byName_.find(name); + if (it == byName_.end()) return kNoDecl; + return it->second; +} + +DeclIndex DeclTable::findByNameAndKind(const std::string &name, + DeclKind kind) const { + // Linear scan over all decls. byName_ only tracks the first + // registration per name; for kind-aware lookup we need to walk + // every entry. The number of decls is bounded by source size so + // this stays O(decls) per call — acceptable for the per-struct / + // per-call invocation pattern. + for (std::size_t i = 1; i < decls_.size(); ++i) { + if (decls_[i].kind == kind && decls_[i].name == name) { + return static_cast(i); + } + } + return kNoDecl; +} + +void DeclTable::declareDependency(DeclIndex from, DeclIndex to) { + if (from == kNoDecl || to == kNoDecl || from == to) return; + auto &f = decls_[from]; + if (std::find(f.dependencies.begin(), f.dependencies.end(), to) == + f.dependencies.end()) { + f.dependencies.push_back(to); + } + auto &t = decls_[to]; + if (std::find(t.dependants.begin(), t.dependants.end(), from) == + t.dependants.end()) { + t.dependants.push_back(from); + } +} + +} // namespace jam diff --git a/src/decl.h b/src/decl.h new file mode 100644 index 0000000..1654fc4 --- /dev/null +++ b/src/decl.h @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2026-present Raphael Amorim + * + * This file is part of jam. + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions. + */ + +#ifndef JAM_DECL_H +#define JAM_DECL_H + +#include "abi.h" +#include "ast_flat.h" +#include +#include +#include +#include + +class FunctionAST; +class StructDeclAST; +class EnumDeclAST; +class UnionDeclAST; +class ConstDeclAST; + +namespace jam { + +// ABI signature of a non-generic function, computed once by the +// analyzer and cached so callers don't re-classify at every site. +// Generic functions skip this cache — each instantiation is its own +// FunctionAST with its own substituted param TypeIdxs, so the cache +// would never hit. `computed` distinguishes "analyzer ran" from the +// default-constructed empty-vector state. +struct FnSignature { + std::vector params; + jam::abi::ReturnABI returnAbi{}; + bool computed = false; +}; + +// Stable index into the per-compilation DeclTable. Index 0 is reserved +// as kNoDecl so callers can use it as a sentinel. +using DeclIndex = uint32_t; +constexpr DeclIndex kNoDecl = 0; + +// What sort of source-level binding produced this Decl. Determines +// which AST pointer is non-null and which analysis branch runs. +enum class DeclKind : uint8_t { + Invalid = 0, + Function, // fn name(...) ret { ... } + extern + tfn + Struct, // const Name = struct { ... }; + Enum, // const Name = enum { ... }; + Union, // const Name = union { ... }; + Const, // const NAME[: T]? = expr; (value, not type alias) + TypeAlias, // const NAME = SomeType / GenericCall, evaluated to a type +}; + +// Where this Decl sits in the analysis lifecycle. The chokepoint state +// is InProgress: re-entering it triggers cycle detection. Mirrors +// Module.zig:451-484's `analysis` enum, trimmed to the states a +// non-incremental compiler needs. +enum class DeclAnalysis : uint8_t { + Unreferenced, // not yet touched + InProgress, // analyzer running, cycle-detector key + Complete, // value populated + AnalysisFailure, // this decl's own analysis failed + DependencyFailure, // a Decl we depended on failed +}; + +// A Struct's *body* lifecycle, distinct from its Decl's value. The +// Decl can be Complete (the Struct's typed value is `(type, NamedTy)`) +// long before the Struct's fields are materialised. Field materialisation +// only happens when something — `getLLVMType`, `@sizeOf`, a field +// access — actually demands it. Mirrors Module.zig:923-933. +enum class StructStatus : uint8_t { + None, // declared, fields not yet computed + FieldTypesWIP, // computing fields; cycle-detector key + HaveFieldTypes, // field TypeIdxs known; LLVM body may still be empty + LayoutWIP, // computing LLVM body / size / align + HaveLayout, // LLVM body emitted; size/align cached +}; + +// Enum / Union body lifecycles. Same shape as StructStatus but kept +// in their own enums so a tagged-union variant's status can't be +// silently treated as a struct's. Both follow None → BodyWIP → +// HaveBody; the WIP state is the cycle-detector key for +// `resolveTypeFieldsEnum` / `resolveTypeFieldsUnion`. +enum class EnumStatus : uint8_t { + None, + BodyWIP, + HaveBody, +}; + +enum class UnionStatus : uint8_t { + None, + BodyWIP, + HaveBody, +}; + +// The value a fully-analyzed Decl resolves to. Tiny mini-Value: Jam's +// generic args today are only types, so we don't need comptime int / +// big-int / pointer-decl-ref variants. Adding them later is just new +// enum cases + payload. +struct DeclValue { + enum class Kind : uint8_t { + None, // not yet computed / failed + Type, // a type value (struct, enum, alias result, builtin) + Function, // a fn reference + }; + Kind kind = Kind::None; + TypeIdx type = kNoType; // when kind == Type + const FunctionAST *function = nullptr; // when kind == Function +}; + +// One source-level declaration. Owned by DeclTable; held by index so +// stable across reallocation. +struct Decl { + std::string name; // user-visible identifier (no namespacing) + DeclKind kind = DeclKind::Invalid; + DeclAnalysis analysis = DeclAnalysis::Unreferenced; + StructStatus structStatus = StructStatus::None; + EnumStatus enumStatus = EnumStatus::None; + UnionStatus unionStatus = UnionStatus::None; + + // Exactly one of these is non-null; which one is determined by kind. + const FunctionAST *fnAst = nullptr; + const StructDeclAST *structAst = nullptr; + const EnumDeclAST *enumAst = nullptr; + const UnionDeclAST *unionAst = nullptr; + const ConstDeclAST *constAst = nullptr; + + // Populated when analysis == Complete. For Struct/Enum/Union/TypeAlias + // this carries the resolved TypeIdx; for Function it carries the + // FunctionAST*; for Const it stays kind==None and the existing + // JamCodegenContext::registerModuleConst path handles the value. + DeclValue value; + + // Cached ABI signature for non-generic functions. Set by + // `Analyzer::analyzeFunction`; reads must check `signature.computed`. + // See `FnSignature` above. + FnSignature signature; + + // Best-effort source position for diagnostics. Resolved at register + // time from the AST node's first token. + int line = 0; + std::string file; + + // Shallow dependency graph: who I depend on and who depends on me. + // Symmetric: declareDependency(A, B) appends to both sides. + std::vector dependencies; + std::vector dependants; +}; + +// Central registry — one per compilation. Holds every named top-level +// binding from the main module and every imported module's pub items. +// Index 0 holds a sentinel Invalid Decl so kNoDecl reads as "not found". +class DeclTable { + public: + DeclTable(); + + // Create a new Decl. Caller fills the *Ast pointer matching `kind` + // after the call. Returns the new index. + DeclIndex create(DeclKind kind, std::string name); + + Decl &get(DeclIndex idx); + const Decl &get(DeclIndex idx) const; + + // Find a Decl by its source-level name. Returns kNoDecl if not + // present. Used by the register pass to wire dependencies and by + // the demand pass to translate a name reference into an index. + DeclIndex findByName(const std::string &name) const; + + // Kind-filtered lookup. Jam permits a function and a type to + // share a name (e.g. `fn Counter(T:type) type` lives in the + // function namespace; `const Counter = struct {...}` lives in + // the type namespace). Pass the kind you want; returns kNoDecl + // if no decl of that kind has the given name. + DeclIndex findByNameAndKind(const std::string &name, DeclKind kind) const; + + // Wire a symmetric `from -> to` dependency edge so trace output + // can walk either direction. Idempotent: a duplicate edge is a + // no-op. Mirrors Module.zig:4839-4859 declareDeclDependency. + void declareDependency(DeclIndex from, DeclIndex to); + + // Number of registered decls excluding the sentinel. + std::size_t size() const { return decls_.size() - 1; } + + // Iteration helpers; caller is expected to skip kNoDecl. + const std::vector &all() const { return decls_; } + + private: + std::vector decls_; + std::unordered_map byName_; +}; + +} // namespace jam + +#endif // JAM_DECL_H diff --git a/src/diagnostics.cpp b/src/diagnostics.cpp index fdec347..180cf75 100644 --- a/src/diagnostics.cpp +++ b/src/diagnostics.cpp @@ -110,7 +110,10 @@ void emitOne(std::ostream &out, const Diagnostic &d, int indent = 0) { emitOne(out, nd, indent + 4); } for (const auto &frame : d.referenceTrace) { - out << pad << " note: in instantiation of `" << frame.decl << "`"; + const char *verb = frame.kind == Diagnostic::Trace::Kind::Reference + ? "referenced by" + : "in instantiation of"; + out << pad << " note: " << verb << " `" << frame.decl << "`"; if (!frame.loc.file.empty() && frame.loc.line > 0) { out << " at " << frame.loc.file << ":" << frame.loc.line; } diff --git a/src/diagnostics.h b/src/diagnostics.h index 584d6c9..83c124e 100644 --- a/src/diagnostics.h +++ b/src/diagnostics.h @@ -40,8 +40,20 @@ struct Diagnostic { enum class Severity : uint8_t { Error, Warning, Note }; struct Trace { + // Distinguishes the verb used when the formatter prints a + // frame. Generic-instantiation frames pushed by codegen + // during monomorphisation use `Instantiation` ("in + // instantiation of `Vec(i32).default`"); analyzer frames + // pushed while walking decl dependencies use `Reference` + // ("referenced by `MyStruct`"). The two convey different + // causal shapes — one is "you asked me to compile X(T)", the + // other is "X showed up while resolving Y" — and conflating + // them under one verb loses information at error time. + enum class Kind : uint8_t { Instantiation, Reference }; + SrcLoc loc; std::string decl; // e.g. "Vec(NoDefault).default" + Kind kind = Kind::Instantiation; // `hidden` counts trace frames that were elided when the // chain exceeded a (future) `--reference-trace=N` limit. // Kept at 0 today: the full live stack is always diff --git a/src/lexer.cpp b/src/lexer.cpp index acaf9d0..e08e068 100644 --- a/src/lexer.cpp +++ b/src/lexer.cpp @@ -71,9 +71,16 @@ void Lexer::addToken(TokenType type) { addToken(type, ""); } void Lexer::addToken(TokenType type, const std::string &lexeme) { // `tokenStart` is captured at the top of each scan-loop iteration - // (before the first `advance()`), so we can stamp it onto every - // emitted Token without per-call site bookkeeping. - tokens.emplace_back(type, lexeme, line, tokenStart); + // (before the first `advance()`); `length` is the span between + // then and `current`. Together they let callers slice the source + // for the token's raw bytes via `Token::text(source)` — no per- + // token string copy. The `lexeme` argument is only passed by the + // string-literal path, which needs to carry the escape-decoded + // value (a `\n` in the raw source becomes a single LF byte that + // the source slice can't reproduce). + uint32_t length = + static_cast(static_cast(current) - tokenStart); + tokens.emplace_back(type, lexeme, line, tokenStart, length); } void Lexer::identifier() { @@ -81,61 +88,66 @@ void Lexer::identifier() { 1; // Start position (we already consumed the first character) while (isAlphaNumeric(peek())) advance(); - std::string text = source.substr(start, current - start); + // `text` is a view into the source — keyword classification only + // reads it, so no copy is needed. The emitted Token's `lexeme` + // stays empty; downstream callers reconstruct the string via + // `token.text(source)`. + std::string_view text(source.data() + start, + static_cast(current - start)); // Check for keywords if (text == "fn") { - addToken(TOK_FN, text); + addToken(TOK_FN); } else if (text == "return") { - addToken(TOK_RETURN, text); + addToken(TOK_RETURN); } else if (text == "const") { - addToken(TOK_CONST, text); + addToken(TOK_CONST); } else if (text == "var") { - addToken(TOK_VAR, text); + addToken(TOK_VAR); } else if (text == "mut") { - addToken(TOK_MUT, text); + addToken(TOK_MUT); } else if (text == "if") { - addToken(TOK_IF, text); + addToken(TOK_IF); } else if (text == "else") { - addToken(TOK_ELSE, text); + addToken(TOK_ELSE); } else if (text == "match") { - addToken(TOK_MATCH, text); + addToken(TOK_MATCH); } else if (text == "while") { - addToken(TOK_WHILE, text); + addToken(TOK_WHILE); } else if (text == "loop") { - addToken(TOK_LOOP, text); + addToken(TOK_LOOP); } else if (text == "for") { - addToken(TOK_FOR, text); + addToken(TOK_FOR); } else if (text == "break") { - addToken(TOK_BREAK, text); + addToken(TOK_BREAK); } else if (text == "continue") { - addToken(TOK_CONTINUE, text); + addToken(TOK_CONTINUE); } else if (text == "in") { - addToken(TOK_IN, text); + addToken(TOK_IN); } else if (text == "true") { - addToken(TOK_TRUE, text); + addToken(TOK_TRUE); } else if (text == "false") { - addToken(TOK_FALSE, text); + addToken(TOK_FALSE); } else if (text == "extern") { - addToken(TOK_EXTERN, text); + addToken(TOK_EXTERN); } else if (text == "export") { - addToken(TOK_EXPORT, text); + addToken(TOK_EXPORT); } else if (text == "pub") { - addToken(TOK_PUB, text); + addToken(TOK_PUB); } else if (text == "import") { - addToken(TOK_IMPORT, text); + addToken(TOK_IMPORT); } else if (text == "tfn") { - addToken(TOK_TFN, text); + addToken(TOK_TFN); } else if (text == "struct") { - addToken(TOK_STRUCT, text); + addToken(TOK_STRUCT); } else if (text == "union") { - addToken(TOK_UNION, text); + addToken(TOK_UNION); } else if (text == "enum") { - addToken(TOK_ENUM, text); + addToken(TOK_ENUM); } else if (text == "as") { - addToken(TOK_AS, text); + addToken(TOK_AS); } else if (text == "move") { - addToken(TOK_MOVE, text); + addToken(TOK_MOVE); } else if (text == "u1" || text == "u8" || text == "u16" || text == "u32" || text == "u64" || text == "i8" || text == "i16" || text == "i32" || text == "i64" || text == "f32" || @@ -145,9 +157,9 @@ void Lexer::identifier() { // themselves types, used at compile time only. Lexed as TOK_TYPE // alongside the scalar built-ins so the parser sees it in a type // position uniformly. - addToken(TOK_TYPE, text); + addToken(TOK_TYPE); } else { - addToken(TOK_IDENTIFIER, text); + addToken(TOK_IDENTIFIER); } } @@ -175,15 +187,14 @@ static bool isAlphaDigit(char c) { (c >= 'A' && c <= 'Z'); } -void Lexer::number() { scanNumberBody(current - 1); } +void Lexer::number() { scanNumberBody(); } void Lexer::negativeNumber() { - int start = current - 1; // points at the leading `-` - advance(); // consume the first digit - scanNumberBody(start); + advance(); // consume the first digit after the leading `-` + scanNumberBody(); } -void Lexer::scanNumberBody(int start) { +void Lexer::scanNumberBody() { // State machine: int → int_period → float → float_exp. We track // only enough state to know whether `+`/`-` is valid (it is only // after a p/P/e/E exponent letter). @@ -261,8 +272,7 @@ void Lexer::scanNumberBody(int start) { if (stop) break; } - std::string num = source.substr(start, current - start); - addToken(TOK_NUMBER, num); + addToken(TOK_NUMBER); } char Lexer::parseHexByte() { diff --git a/src/lexer.h b/src/lexer.h index 80b1f4a..c1a8c61 100644 --- a/src/lexer.h +++ b/src/lexer.h @@ -38,7 +38,7 @@ class Lexer { void identifier(); void number(); void negativeNumber(); - void scanNumberBody(int start); + void scanNumberBody(); void stringLiteral(); char parseHexByte(); std::string parseUnicodeEscape(); @@ -46,6 +46,13 @@ class Lexer { public: explicit Lexer(std::string source); std::vector scanTokens(); + + // Const view of the source buffer the lexer holds. Used by the + // parser to resolve `Token::text(source)` calls — every emitted + // Token's `byteOffset`/`length` index into THIS buffer, so the + // parser must reference the lexer's owned copy (or another + // identical copy, but reusing the lexer's avoids a duplicate). + const std::string &sourceBuffer() const { return source; } }; #endif // LEXER_H diff --git a/src/main.cpp b/src/main.cpp index 62dad0c..466041f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,6 +17,7 @@ #include #include +#include "analyzer.h" #include "ast.h" #include "astgen.h" #include "cabi.h" @@ -97,9 +98,9 @@ static int compileAndRun(const std::string &filename, std::vector> sharedAnonStructs; std::vector> sharedAnonEnums; - Parser parser(tokens, codegenCtx.getTypePool(), codegenCtx.getStringPool(), - codegenCtx.getNodeStore(), &codegenCtx.diagnostics(), - filename); + Parser parser(tokens, lexer.sourceBuffer(), codegenCtx.getTypePool(), + codegenCtx.getStringPool(), codegenCtx.getNodeStore(), + &codegenCtx.diagnostics(), filename); parser.sharedAnonStructs = &sharedAnonStructs; parser.sharedAnonEnums = &sharedAnonEnums; std::unique_ptr module; @@ -158,6 +159,70 @@ static int compileAndRun(const std::string &filename, codegenCtx.setAnonStructs(&sharedAnonStructs); codegenCtx.setAnonEnums(&sharedAnonEnums); + + // Populate the demand-driven DeclTable: one Decl per top-level + // binding across the main module + every imported module. The + // analyzer consults this table for cycle detection when codegen + // resolves a generic call, a Named type, or any other cross-decl + // reference. Step 4 will make this the *only* source of truth and + // delete the parallel functionAsts / struct registry tables; for + // now we populate it in parallel so the existing eager pipeline + // stays unchanged. + auto registerTopLevelDecls = [&](ModuleAST *m, bool publicOnly) { + auto setSrc = [&](jam::Decl &d, const std::string &name) { + (void)name; + d.file = codegenCtx.currentFile(); + // Line populated when AST nodes carry per-node line; for + // the top-level decl we use 0 (no specific line) for + // now. + }; + for (auto &fn : m->Functions) { + if (publicOnly && !fn->isPub) continue; + jam::DeclIndex idx = codegenCtx.declTable().create( + jam::DeclKind::Function, fn->Name); + auto &d = codegenCtx.declTable().get(idx); + d.fnAst = fn.get(); + setSrc(d, fn->Name); + } + for (auto &s : m->Structs) { + if (publicOnly && !s->isPub) continue; + jam::DeclIndex idx = + codegenCtx.declTable().create(jam::DeclKind::Struct, s->Name); + auto &d = codegenCtx.declTable().get(idx); + d.structAst = s.get(); + setSrc(d, s->Name); + } + for (auto &e : m->Enums) { + if (publicOnly && !e->isPub) continue; + jam::DeclIndex idx = + codegenCtx.declTable().create(jam::DeclKind::Enum, e->Name); + auto &d = codegenCtx.declTable().get(idx); + d.enumAst = e.get(); + setSrc(d, e->Name); + } + for (auto &u : m->Unions) { + if (publicOnly && !u->isPub) continue; + jam::DeclIndex idx = + codegenCtx.declTable().create(jam::DeclKind::Union, u->Name); + auto &d = codegenCtx.declTable().get(idx); + d.unionAst = u.get(); + setSrc(d, u->Name); + } + for (auto &c : m->Consts) { + if (publicOnly && !c->isPub) continue; + jam::DeclIndex idx = + codegenCtx.declTable().create(jam::DeclKind::Const, c->Name); + auto &d = codegenCtx.declTable().get(idx); + d.constAst = c.get(); + setSrc(d, c->Name); + } + }; + for (const auto &[path, importedModule] : resolver.getLoadedModules()) { + if (path == "std") continue; + registerTopLevelDecls(importedModule.get(), /*publicOnly=*/true); + } + registerTopLevelDecls(module.get(), /*publicOnly=*/false); + // `publicOnly` is set when iterating an imported module: only `pub` // items get registered, so non-pub items can't leak into the // importing module via bare-name lookup. Main-module decls always @@ -178,66 +243,6 @@ static int compileAndRun(const std::string &filename, codegenCtx.registerUnion(u->Name, unionType, u->Fields); } }; - auto fillStructBodies = [&](ModuleAST *m, bool publicOnly) { - for (auto &s : m->Structs) { - if (publicOnly && !s->isPub) continue; - std::vector fieldTypes; - fieldTypes.reserve(s->Fields.size()); - for (auto &f : s->Fields) { - fieldTypes.push_back(codegenCtx.getLLVMType(f.second)); - } - const auto *info = codegenCtx.getStruct(s->Name); - JamLLVMStructSetBody(info->type, fieldTypes.data(), - static_cast(fieldTypes.size()), - false); - } - }; - // Lay out a union as `{ alignedField, [paddingBytes x i8] }`. The - // alignedField is the field with the largest alignment requirement; - // padding makes up the difference between that field's size and the - // largest field's size, so the union ends up with the right size and - // alignment for any field's stored value. - auto fillUnionBodies = [&](ModuleAST *m, bool publicOnly) { - for (auto &u : m->Unions) { - if (publicOnly && !u->isPub) continue; - if (u->Fields.empty()) { - throw std::runtime_error("Union `" + u->Name + - "` must have at least one field"); - } - uint64_t maxSize = 0, maxAlign = 1; - size_t alignFieldIdx = 0; - for (size_t i = 0; i < u->Fields.size(); i++) { - uint64_t sz = codegenCtx.typeSize(u->Fields[i].second); - uint64_t al = codegenCtx.typeAlign(u->Fields[i].second); - if (sz > maxSize) maxSize = sz; - if (al > maxAlign) { - maxAlign = al; - alignFieldIdx = i; - } - } - // Round size up to a multiple of alignment so writes through - // the most-aligned field don't run off the end. - uint64_t allocSize = (maxSize + maxAlign - 1) / maxAlign * maxAlign; - JamTypeRef alignedTy = - codegenCtx.getLLVMType(u->Fields[alignFieldIdx].second); - uint64_t alignedSz = - codegenCtx.typeSize(u->Fields[alignFieldIdx].second); - uint64_t paddingBytes = - allocSize > alignedSz ? allocSize - alignedSz : 0; - - std::vector bodyTypes; - bodyTypes.push_back(alignedTy); - if (paddingBytes > 0) { - bodyTypes.push_back( - JamLLVMArrayType(codegenCtx.getInt8Type(), - static_cast(paddingBytes))); - } - const auto *info = codegenCtx.getUnion(u->Name); - JamLLVMStructSetBody(info->type, bodyTypes.data(), - static_cast(bodyTypes.size()), - false); - } - }; auto declareEnums = [&](ModuleAST *m, bool publicOnly) { for (auto &e : m->Enums) { if (publicOnly && !e->isPub) continue; @@ -253,93 +258,6 @@ static int compileAndRun(const std::string &filename, codegenCtx.registerEnum(e->Name, std::move(variants)); } }; - // For payloaded enums, lay out as `{i8 tag, alignDriver, - // [extraBytes x i8]}` where `alignDriver` is the smallest scalar - // type whose alignment matches the strictest variant's alignment - // (i8 / i16 / i32 / i64 for align 1 / 2 / 4 / 8 respectively). - // LLVM gives the resulting struct the alignment of `alignDriver`, - // which propagates to allocas and stores — without that we get - // align-1 enum slots even for u64-payload variants, which forces - // LLVM to emit unaligned memory ops. - // - // `extraBytes` makes up the difference between the largest variant's - // payload size and the alignDriver scalar's size, rounded up to - // maxAlign so the trailing slot in an array of enums is correctly - // aligned. - auto fillEnumBodies = [&](ModuleAST *m, bool publicOnly) { - for (auto &e : m->Enums) { - if (publicOnly && !e->isPub) continue; - const auto *info = codegenCtx.getEnum(e->Name); - if (!info || !info->hasPayloadVariant) continue; - - uint64_t maxSize = 0, maxAlign = 1; - for (const auto &v : info->variants) { - uint64_t off = 0, varAlign = 1; - for (TypeIdx t : v.payloadTypes) { - uint64_t s = codegenCtx.typeSize(t); - uint64_t a = codegenCtx.typeAlign(t); - off = (off + a - 1) / a * a; // align this field - off += s; - if (a > varAlign) varAlign = a; - } - if (varAlign > 1) { - off = (off + varAlign - 1) / varAlign * varAlign; - } - if (off > maxSize) maxSize = off; - if (varAlign > maxAlign) maxAlign = varAlign; - } - - // Pick a scalar to drive struct alignment. - JamTypeRef alignDriver; - uint64_t alignDriverSize; - switch (maxAlign) { - case 1: - alignDriver = codegenCtx.getInt8Type(); - alignDriverSize = 1; - break; - case 2: - alignDriver = codegenCtx.getInt16Type(); - alignDriverSize = 2; - break; - case 4: - alignDriver = codegenCtx.getInt32Type(); - alignDriverSize = 4; - break; - case 8: - alignDriver = codegenCtx.getInt64Type(); - alignDriverSize = 8; - break; - default: - throw std::runtime_error( - "Enum `" + e->Name + - "` requires alignment > 8, which is not yet " - "supported"); - } - - // Round payload size up to maxAlign so a contiguous array - // of enums tiles correctly. - uint64_t paddedSize = - (maxSize + maxAlign - 1) / maxAlign * maxAlign; - uint64_t extraBytes = (paddedSize > alignDriverSize) - ? paddedSize - alignDriverSize - : 0; - - std::vector bodyTypes; - bodyTypes.push_back(codegenCtx.getInt8Type()); // tag - bodyTypes.push_back(alignDriver); - if (extraBytes > 0) { - bodyTypes.push_back( - JamLLVMArrayType(codegenCtx.getInt8Type(), - static_cast(extraBytes))); - } - - JamLLVMStructSetBody(info->type, bodyTypes.data(), - static_cast(bodyTypes.size()), - false); - codegenCtx.setEnumLLVMType(e->Name, info->type, maxSize, maxAlign, - true); - } - }; // Enums that need a named struct type (i.e. those with payload // variants) get their LLVM type created here, in declareEnums, so // that fillEnumBodies can set the body in a second pass. @@ -363,15 +281,80 @@ static int compileAndRun(const std::string &filename, declareUnions(module.get(), /*publicOnly=*/false); declareEnums(module.get(), /*publicOnly=*/false); declareEnumLLVMTypes(module.get()); + + // Register imported pub functions + handle-import metadata + emit + // their LLVM prototypes BEFORE filling struct/union/enum bodies. + // A struct field typed as a generic instantiation + // (`inner: Vec(i32)`) drives `getLLVMType` to resolve the + // GenericCall, which instantiates Vec for i32 and codegens its + // 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. for (const auto &[path, importedModule] : resolver.getLoadedModules()) { if (path == "std") continue; - fillStructBodies(importedModule.get(), /*publicOnly=*/true); - fillUnionBodies(importedModule.get(), /*publicOnly=*/true); - fillEnumBodies(importedModule.get(), /*publicOnly=*/true); + 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()); + } + } + } + for (auto &import : module->Imports) { + if (import->Path == "std" || import->Path == "test") continue; + const std::string &handle = import->Name; + ModuleAST *importedModule = resolver.getOrLoadModule(import->Path); + if (!importedModule) continue; + codegenCtx.registerImportHandle(handle, import->Path); + auto aliasNamed = [&](const std::string &bare) { + TypeIdx target = codegenCtx.getTypePool().internNamed( + codegenCtx.getStringPool().intern(bare)); + codegenCtx.registerTypeAlias(handle + "." + bare, target); + }; + for (auto &func : importedModule->Functions) { + if (func->isPub) { + codegenCtx.registerFunctionAST(handle + "." + func->Name, + func.get()); + } else { + codegenCtx.registerPrivateName(handle, func->Name); + } + } + for (auto &s : importedModule->Structs) { + if (s->isPub) aliasNamed(s->Name); + else codegenCtx.registerPrivateName(handle, s->Name); + } + for (auto &e : importedModule->Enums) { + if (e->isPub) aliasNamed(e->Name); + else codegenCtx.registerPrivateName(handle, e->Name); + } + for (auto &u : importedModule->Unions) { + if (u->isPub) aliasNamed(u->Name); + else codegenCtx.registerPrivateName(handle, u->Name); + } + } + + // 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); + } } - fillStructBodies(module.get(), /*publicOnly=*/false); - fillUnionBodies(module.get(), /*publicOnly=*/false); - fillEnumBodies(module.get(), /*publicOnly=*/false); // Register module-scope `const NAME[: T]? = expr;` bindings. These // are inlined at use sites (see AstTag::Variable in ast.cpp), so we @@ -497,61 +480,10 @@ static int compileAndRun(const std::string &filename, // file (or another module) would fail with "Unknown function". The // two-pass shape lets source read naturally top-down (main on top, // helpers below) without a manual "forward declarations" section. - - // Pass 1a: prototypes for pub functions in imported modules. - for (const auto &[path, importedModule] : resolver.getLoadedModules()) { - if (path == "std") continue; - 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); - } - // Generic functions are still registered so call sites can - // look them up for instantiation, but no LLVM is emitted - // until each instantiation is processed. - if (func->isPub) { - codegenCtx.registerFunctionAST(func->Name, func.get()); - } - } - } - - for (auto &import : module->Imports) { - if (import->Path == "std" || import->Path == "test") continue; - const std::string &handle = import->Name; - ModuleAST *importedModule = resolver.getOrLoadModule(import->Path); - if (!importedModule) continue; - codegenCtx.registerImportHandle(handle, import->Path); - auto aliasNamed = [&](const std::string &bare) { - TypeIdx target = codegenCtx.getTypePool().internNamed( - codegenCtx.getStringPool().intern(bare)); - codegenCtx.registerTypeAlias(handle + "." + bare, target); - }; - // Track pub vs non-pub items per handle. Pub items get the - // qualified registration; non-pub get recorded so a later - // lookup miss produces the precise "not exported" diagnostic. - for (auto &func : importedModule->Functions) { - if (func->isPub) { - codegenCtx.registerFunctionAST(handle + "." + func->Name, - func.get()); - } else { - codegenCtx.registerPrivateName(handle, func->Name); - } - } - for (auto &s : importedModule->Structs) { - if (s->isPub) aliasNamed(s->Name); - else codegenCtx.registerPrivateName(handle, s->Name); - } - for (auto &e : importedModule->Enums) { - if (e->isPub) aliasNamed(e->Name); - else codegenCtx.registerPrivateName(handle, e->Name); - } - for (auto &u : importedModule->Unions) { - if (u->isPub) aliasNamed(u->Name); - else codegenCtx.registerPrivateName(handle, u->Name); - } - } + // + // Pass 1a (imported pub fn prototypes) and the handle-import + // metadata loop ran earlier — see above declareStructs — because + // struct-field generic instantiation needs them to be in place. // Pass 1b: prototypes for the main module's functions (we still skip // test funcs in non-test mode and user `main` in test mode). diff --git a/src/module_resolver.cpp b/src/module_resolver.cpp index f7a7d59..55aefa6 100644 --- a/src/module_resolver.cpp +++ b/src/module_resolver.cpp @@ -156,7 +156,8 @@ std::unique_ptr ModuleResolver::parseSource(const std::string &source) const { Lexer lexer(source); std::vector tokens = lexer.scanTokens(); - Parser parser(tokens, *typePool, *stringPool, *nodeStore); + Parser parser(tokens, lexer.sourceBuffer(), *typePool, *stringPool, + *nodeStore); parser.sharedAnonStructs = sharedAnonStructs_; parser.sharedAnonEnums = sharedAnonEnums_; return parser.parse(); diff --git a/src/number_literal.cpp b/src/number_literal.cpp index c2449ec..5097901 100644 --- a/src/number_literal.cpp +++ b/src/number_literal.cpp @@ -47,7 +47,7 @@ NumberResult okFloat(NumberBase base, double value) { } // namespace -NumberResult parseNumberLiteral(const std::string &bytes) { +NumberResult parseNumberLiteral(std::string_view bytes) { std::size_t i = 0; uint8_t baseValue = 10; NumberBase base = NumberBase::Decimal; diff --git a/src/number_literal.h b/src/number_literal.h index 9759942..e127796 100644 --- a/src/number_literal.h +++ b/src/number_literal.h @@ -11,6 +11,7 @@ #include #include #include +#include enum class NumberBase : uint8_t { Decimal = 10, @@ -63,7 +64,7 @@ struct NumberResult { NumberError failure{}; }; -NumberResult parseNumberLiteral(const std::string &bytes); +NumberResult parseNumberLiteral(std::string_view bytes); const char *numberErrorMessage(NumberErrorKind kind); #endif // NUMBER_LITERAL_H diff --git a/src/parser.cpp b/src/parser.cpp index ca2266e..a563401 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -17,10 +17,10 @@ // Returns the 64-bit magnitude. For float literals the bit pattern of // the parsed `double` is returned via `bit_cast`, and `isFloatOut` is // set so the caller can mark the AST node with the float flag. -uint64_t Parser::parseNumLexeme(const std::string &s, bool &isNegOut, +uint64_t Parser::parseNumLexeme(std::string_view s, bool &isNegOut, bool &isFloatOut) const { bool neg = !s.empty() && s[0] == '-'; - const std::string &abs = neg ? s.substr(1) : s; + std::string_view abs = neg ? s.substr(1) : s; isNegOut = neg; isFloatOut = false; @@ -29,7 +29,8 @@ uint64_t Parser::parseNumLexeme(const std::string &s, bool &isNegOut, case NumberResultKind::Int: return r.intValue; case NumberResultKind::BigInt: - parseError("integer literal `" + abs + "` exceeds u64 range"); + parseError("integer literal `" + std::string(abs) + + "` exceeds u64 range"); case NumberResultKind::Float: { isFloatOut = true; // pack the double's bit pattern into u64. memcpy keeps it @@ -42,17 +43,17 @@ uint64_t Parser::parseNumLexeme(const std::string &s, bool &isNegOut, return bits; } case NumberResultKind::Failure: - parseError(std::string("invalid numeric literal `") + abs + + parseError(std::string("invalid numeric literal `") + std::string(abs) + "`: " + numberErrorMessage(r.failure.kind)); } parseError("unreachable number-literal classification"); } -Parser::Parser(std::vector tokens, TypePool &typePool_, - StringPool &stringPool_, NodeStore &nodes_, +Parser::Parser(std::vector tokens, const std::string &source, + TypePool &typePool_, StringPool &stringPool_, NodeStore &nodes_, jam::Diagnostics *diagnostics, std::string filename) - : tokens(std::move(tokens)), typePool(&typePool_), stringPool(&stringPool_), - nodes(&nodes_), diagnostics_(diagnostics), + : tokens(std::move(tokens)), source_(source), typePool(&typePool_), + stringPool(&stringPool_), nodes(&nodes_), diagnostics_(diagnostics), filename_(std::move(filename)) {} jam::SrcLoc Parser::currentLoc() const { @@ -146,6 +147,29 @@ bool Parser::isQualifiedNameChain(NodeIdx chainRoot) const { return false; } +// 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) { + const AstNode &n = ns.get(chainRoot); + if (n.tag == AstTag::Variable) { + return pool.get(static_cast(n.lhs)); + } + if (n.tag == AstTag::MemberAccess) { + return chainRootName(ns, pool, static_cast(n.lhs)); + } + return std::string(); +} + +// Count the number of dots (== MemberAccess hops) in a qualified-name +// chain. `foo` → 0, `foo.bar` → 1, `foo.bar.baz` → 2, etc. +static int chainDotCount(const NodeStore &ns, NodeIdx chainRoot) { + const AstNode &n = ns.get(chainRoot); + if (n.tag != AstTag::MemberAccess) return 0; + return 1 + chainDotCount(ns, static_cast(n.lhs)); +} + NodeIdx Parser::parsePrimary() { // `match (…) { … }` is also valid in expression position so it can // produce a value. The same call works for both statement and @@ -160,7 +184,7 @@ NodeIdx Parser::parsePrimary() { // + arbitrary value args arrive in Stage 2 with CTFE. if (match(TOK_AT)) { consume(TOK_IDENTIFIER, "Expected intrinsic name after '@'"); - StringIdx nameId = stringPool->intern(previous().lexeme); + StringIdx nameId = stringPool->intern(previous().text(source_)); consume(TOK_OPEN_PAREN, "Expected '(' after '@name'"); TypeIdx tyArg = parseType(); consume(TOK_CLOSE_PAREN, "Expected ')' after '@' intrinsic argument"); @@ -174,7 +198,8 @@ NodeIdx Parser::parsePrimary() { // set the magnitude is the bit pattern of a `double`). bool isNegative = false; bool isFloat = false; - uint64_t mag = parseNumLexeme(previous().lexeme, isNegative, isFloat); + uint64_t mag = + parseNumLexeme(previous().text(source_), isNegative, isFloat); uint16_t flags = 0; if (isNegative) flags |= 1; if (isFloat) flags |= 2; @@ -193,6 +218,10 @@ NodeIdx Parser::parsePrimary() { return emit(AstNode{AstTag::BoolLit, 0, 0, 0, 0, 0}); } if (match(TOK_STRING_LITERAL)) { + // String literals carry decoded bytes (escape sequences + // resolved); `text(source_)` would return the raw `"..."`- + // bracketed source including escapes — wrong for callers + // that want the runtime string value. StringIdx s = stringPool->intern(previous().lexeme); return emit(AstNode{AstTag::StringLit, 0, 0, 0, s, 0}); } @@ -208,7 +237,16 @@ NodeIdx Parser::parsePrimary() { consume(TOK_CLOSE_PAREN, "Expected ')' after expression"); return expr; } - if (match(TOK_OPEN_BRACE)) { return parseStructLiteral(); } + // Bare `{ field: val }` is no longer a valid expression. Struct + // literals always carry their type: `Foo { field: val }` or + // `Vec(i32) { field: val }` (handled when an identifier precedes + // the brace, in the TOK_IDENTIFIER branch below). A naked brace + // in expression position is a parse error. + if (check(TOK_OPEN_BRACE)) { + parseError("Struct literals must name their type: write " + "`TypeName { ... }` (or `Self { ... }` inside a " + "struct body)"); + } if (match(TOK_OPEN_BRACKET)) { // Array literal `[a, b, c]`, array repeat `[expr; N]`, or empty // `[]` (well-typed only against a slice or zero-length array @@ -259,8 +297,34 @@ NodeIdx Parser::parsePrimary() { // diagnostic — same outcome as today's grammar would have given // for `const X = i32;`. if (match(TOK_TYPE) || match(TOK_IDENTIFIER)) { - std::string name = previous().lexeme; + std::string name(previous().text(source_)); StringIdx nameId = stringPool->intern(name); + + // Typed struct literal: `Name { field: val, ... }`. Only + // admitted in expression position when struct literals are + // allowed (see `allowStructLit_`); inside `if`/`while`/`for`/ + // `match` heads the same shape stays as a Variable read + // followed by the head's `{` block opener. + if (allowStructLit_ && check(TOK_OPEN_BRACE)) { + advance(); // consume '{' + NodeIdx lit = parseStructLiteral(); + // Resolve `Self` against the current struct context so + // `return Self { ... }` works inside a method body. For + // any other identifier we intern the Named TypeIdx as-is + // — codegen does the struct/enum lookup at use time. + StringIdx typeNameId = nameId; + if (name == "Self") { + if (structContextStack.empty()) { + parseError("`Self` is only valid inside a struct body"); + } + typeNameId = stringPool->intern(structContextStack.back()); + } + AstNode &litNode = nodes->getMut(lit); + litNode.lhs = static_cast( + typePool->internNamed(typeNameId)); + return lit; + } + NodeIdx expr = emit(AstNode{AstTag::Variable, 0, 0, 0, nameId, 0}); bool chainStarted = false; @@ -274,7 +338,8 @@ NodeIdx Parser::parsePrimary() { static_cast(expr), 0}); } else { consume(TOK_IDENTIFIER, "Expected member name after '.'"); - StringIdx mem = stringPool->intern(previous().lexeme); + StringIdx mem = + stringPool->intern(previous().text(source_)); expr = emit(AstNode{AstTag::MemberAccess, 0, 0, 0, static_cast(expr), mem}); } @@ -293,7 +358,8 @@ NodeIdx Parser::parsePrimary() { // TOK_OPEN_PAREN: typecall on a generic — either bare // `Foo(T).method(args)` (chainStarted=false) or namespace- // qualified `handle.Foo(T).method(args)` when `handle` was - // bound by `import(...)`. + // bound by `import(...)`. Also the generic-struct-literal + // shape `Foo(T) { field: val }`. bool isNamespacedTypecall = chainStarted && importHandles.count(name) > 0; if (!chainStarted || isNamespacedTypecall) { @@ -306,6 +372,36 @@ NodeIdx Parser::parsePrimary() { if (depth == 0) break; peekIdx++; } + // Generic struct literal: `Foo(T) { ... }`. The matching + // `)` is immediately followed by `{`. Parse the parens + // as type arguments, intern as a GenericCall TypeIdx, + // then hand off to parseStructLiteral so the body uses + // the same name:value layout as a non-generic literal. + if (allowStructLit_ && depth == 0 && + peekIdx + 1 < (int)tokens.size() && + tokens[peekIdx].type == TOK_CLOSE_PAREN && + tokens[peekIdx + 1].type == TOK_OPEN_BRACE) { + advance(); // consume ( + std::vector typeArgs; + if (!check(TOK_CLOSE_PAREN)) { + do { + typeArgs.push_back(parseType()); + } while (match(TOK_COMMA)); + } + consume(TOK_CLOSE_PAREN, + "Expected ')' after type arguments"); + consume(TOK_OPEN_BRACE, + "Expected '{' after generic type arguments"); + std::string receiverName = + isNamespacedTypecall ? qualifiedName(expr) : name; + TypeIdx genericTy = typePool->internGenericCall( + stringPool->intern(receiverName), + std::move(typeArgs)); + NodeIdx lit = parseStructLiteral(); + AstNode &litNode = nodes->getMut(lit); + litNode.lhs = static_cast(genericTy); + return lit; + } if (depth == 0 && peekIdx + 3 < (int)tokens.size() && tokens[peekIdx].type == TOK_CLOSE_PAREN && tokens[peekIdx + 1].type == TOK_DOT && @@ -323,7 +419,7 @@ NodeIdx Parser::parsePrimary() { consume(TOK_DOT, "Expected '.' after generic call"); consume(TOK_IDENTIFIER, "Expected method name after '.'"); StringIdx methodName = - stringPool->intern(previous().lexeme); + stringPool->intern(previous().text(source_)); consume(TOK_OPEN_PAREN, "Expected '(' after method name"); std::vector methodArgs; @@ -377,15 +473,48 @@ NodeIdx Parser::parsePrimary() { } if (isQualifiedNameChain(expr)) { - std::string callee; - const AstNode &en = nodes->get(expr); - if (en.tag == AstTag::MemberAccess) { - callee = qualifiedName(expr); + // Qualified-name chains can be one of: + // 1. `mod.fn(...)` / `mod.submod.fn(...)` — module- + // handle prefixed call; root is in `importHandles`. + // 2. `Type.method(...)` — static dispatch on a type + // name; single dot, root is a struct/enum/alias. + // 3. `local.field.method(...)` — instance dispatch + // with a multi-segment receiver path; root is a + // local variable in the caller's scope. + // + // Cases 1 and 2 stay as direct calls (lhs = qualified + // StringIdx); astgen resolves them via the function + // registry or the Type.method handler. Case 3 only + // works through the indirect-call path (flags & 1): + // astgen needs the MemberAccess chain so it can lower + // `local.field` as a value-producing expression and + // dispatch the suffix as a method on that value. + // + // We can't tell at parse time whether the root is a + // local — the parser has no scope info. But we know + // (a) what's in `importHandles`, and (b) the dot + // count. A multi-dot chain whose root is NOT an + // import handle is case 3 by elimination: case 1 + // would need an import handle, case 2 is single-dot. + int dotCount = chainDotCount(*nodes, expr); + std::string root = chainRootName(*nodes, *stringPool, expr); + bool rootIsImportHandle = importHandles.count(root) > 0; + bool isMultiDotLocalChain = + dotCount >= 2 && !rootIsImportHandle; + if (isMultiDotLocalChain) { + expr = emit(AstNode{AstTag::Call, 0, 1, 0, + static_cast(expr), extra}); } else { - callee = name; + std::string callee; + const AstNode &en = nodes->get(expr); + if (en.tag == AstTag::MemberAccess) { + callee = qualifiedName(expr); + } else { + callee = name; + } + StringIdx calleeId = stringPool->intern(callee); + expr = emit(AstNode{AstTag::Call, 0, 0, 0, calleeId, extra}); } - StringIdx calleeId = stringPool->intern(callee); - expr = emit(AstNode{AstTag::Call, 0, 0, 0, calleeId, extra}); } else { expr = emit(AstNode{AstTag::Call, 0, 1, 0, static_cast(expr), extra}); @@ -393,6 +522,30 @@ NodeIdx Parser::parsePrimary() { chainStarted = true; } + // Qualified-name struct literal: `lib.Private { field: val }`. + // After the chain loop finishes, if the expression is a + // MemberAccess chain rooted at a Variable (no calls/indices + // in between) and the next token is `{`, the chain names a + // type. Build the Named TypeIdx from the dotted form and + // parse the body. Same shape as the simple `Name { ... }` + // path above, but the type is the multi-segment qualified + // name so codegen looks it up via the import-handle alias. + // Mirrors Rust's `path::Type { ... }`. + if (allowStructLit_ && check(TOK_OPEN_BRACE) && + isQualifiedNameChain(expr)) { + const AstNode &exprNode = nodes->get(expr); + if (exprNode.tag == AstTag::MemberAccess) { + advance(); // consume '{' + std::string qname = qualifiedName(expr); + TypeIdx namedTy = + typePool->internNamed(stringPool->intern(qname)); + NodeIdx lit = parseStructLiteral(); + AstNode &litNode = nodes->getMut(lit); + litNode.lhs = static_cast(namedTy); + return lit; + } + } + return expr; } @@ -443,12 +596,13 @@ TypeIdx Parser::parseType() { } // `[N]T` — fixed-size array. No tag. consume(TOK_NUMBER, "Expected size or `]` after `[`"); - uint32_t len = static_cast(std::stoul(previous().lexeme)); + uint32_t len = static_cast( + std::stoul(std::string(previous().text(source_)))); consume(TOK_CLOSE_BRACKET, "Expected `]` after array size"); return typePool->internArray(parseType(), len); } if (match(TOK_TYPE)) { - const std::string &s = previous().lexeme; + std::string_view s = previous().text(source_); if (s == "u8") return BuiltinType::U8; if (s == "i8") return BuiltinType::I8; if (s == "u16") return BuiltinType::U16; @@ -463,10 +617,10 @@ TypeIdx Parser::parseType() { if (s == "str") return typePool->internSlice(BuiltinType::U8); if (s == "type") return BuiltinType::Type; if (s == "noreturn") return BuiltinType::NoReturn; - parseError("Unknown base type: " + s); + parseError("Unknown base type: " + std::string(s)); } if (match(TOK_IDENTIFIER)) { - const std::string &firstIdent = previous().lexeme; + std::string_view firstIdent = previous().text(source_); if (firstIdent == "Self") { if (structContextStack.empty()) { parseError("`Self` is only valid inside a struct body"); @@ -478,10 +632,11 @@ TypeIdx Parser::parseType() { // registers each main-module import's pub items under // `.` so the existing struct/enum/union/type-alias // and generic-fn lookups resolve the qualified form transparently. - std::string ident = firstIdent; + std::string ident(firstIdent); if (match(TOK_DOT)) { consume(TOK_IDENTIFIER, "Expected type name after `.`"); - ident = firstIdent + "." + previous().lexeme; + ident = std::string(firstIdent) + "." + + std::string(previous().text(source_)); } if (check(TOK_OPEN_PAREN)) { advance(); // consume `(` @@ -505,7 +660,7 @@ NodeIdx Parser::parseStructLiteral() { std::vector> fields; while (!check(TOK_CLOSE_BRACE) && !isAtEnd()) { consume(TOK_IDENTIFIER, "Expected field name in struct literal"); - StringIdx fieldName = stringPool->intern(previous().lexeme); + StringIdx fieldName = stringPool->intern(previous().text(source_)); consume(TOK_COLON, "Expected ':' after field name"); NodeIdx value = parseLogicalOr(); fields.emplace_back(fieldName, value); @@ -525,7 +680,7 @@ NodeIdx Parser::parseStructLiteral() { } NodeIdx Parser::parsePatternAtom() { - if (check(TOK_IDENTIFIER) && peek().lexeme != "_") { + if (check(TOK_IDENTIFIER) && peek().text(source_) != "_") { int peekIdx = current + 1; if (peekIdx < (int)tokens.size() && tokens[peekIdx].type == TOK_OPEN_PAREN) { @@ -541,7 +696,7 @@ NodeIdx Parser::parsePatternAtom() { tokens[scan].type == TOK_CLOSE_PAREN && tokens[scan + 1].type == TOK_DOT && tokens[scan + 2].type == TOK_IDENTIFIER) { - std::string typeName = peek().lexeme; + std::string typeName(peek().text(source_)); advance(); // IDENT advance(); // `(` std::vector typeArgs; @@ -554,7 +709,8 @@ NodeIdx Parser::parsePatternAtom() { "Expected ')' after generic type arguments"); consume(TOK_DOT, "Expected '.' after generic type"); consume(TOK_IDENTIFIER, "Expected variant name after `.`"); - StringIdx variantNameId = stringPool->intern(previous().lexeme); + StringIdx variantNameId = + stringPool->intern(previous().text(source_)); TypeIdx receiverTy = typePool->internGenericCall( stringPool->intern(typeName), std::move(typeArgs)); @@ -565,7 +721,7 @@ NodeIdx Parser::parsePatternAtom() { consume(TOK_IDENTIFIER, "Expected binding name in variant payload"); bindings.push_back( - stringPool->intern(previous().lexeme)); + stringPool->intern(previous().text(source_))); } while (match(TOK_COMMA)); } consume(TOK_CLOSE_PAREN, @@ -591,13 +747,15 @@ NodeIdx Parser::parsePatternAtom() { } } - if (check(TOK_IDENTIFIER) && peek().lexeme != "_") { + if (check(TOK_IDENTIFIER) && peek().text(source_) != "_") { int saved = current; advance(); // consume enum name if (match(TOK_DOT)) { consume(TOK_IDENTIFIER, "Expected variant name after `.`"); - StringIdx enumNameId = stringPool->intern(tokens[saved].lexeme); - StringIdx variantNameId = stringPool->intern(previous().lexeme); + StringIdx enumNameId = + stringPool->intern(tokens[saved].text(source_)); + StringIdx variantNameId = + stringPool->intern(previous().text(source_)); // Optional payload binding: `(name1, name2, ...)`. Empty // list `()` is permitted and equivalent to no parens. @@ -608,7 +766,7 @@ NodeIdx Parser::parsePatternAtom() { consume(TOK_IDENTIFIER, "Expected binding name in variant payload"); bindings.push_back( - stringPool->intern(previous().lexeme)); + stringPool->intern(previous().text(source_))); } while (match(TOK_COMMA)); } consume(TOK_CLOSE_PAREN, @@ -636,7 +794,8 @@ NodeIdx Parser::parsePatternAtom() { if (match(TOK_NUMBER)) { bool isNegative = false; bool isFloat = false; - uint64_t lo = parseNumLexeme(previous().lexeme, isNegative, isFloat); + uint64_t lo = + parseNumLexeme(previous().text(source_), isNegative, isFloat); if (isFloat) { parseError("Float literals are not allowed in `match` patterns " "(use an integer literal or a `..=` range)"); @@ -646,7 +805,8 @@ NodeIdx Parser::parsePatternAtom() { consume(TOK_NUMBER, "Expected upper bound after `..=`"); bool hiNeg = false; bool hiFloat = false; - uint64_t hi = parseNumLexeme(previous().lexeme, hiNeg, hiFloat); + uint64_t hi = + parseNumLexeme(previous().text(source_), hiNeg, hiFloat); if (hiFloat) { parseError( "Float literals are not allowed in `match` patterns"); @@ -668,7 +828,7 @@ NodeIdx Parser::parsePatternAtom() { parseError("Char literals in patterns are not yet supported"); } // Wildcard `_` is lexed as TOK_IDENTIFIER; recognize it here. - if (check(TOK_IDENTIFIER) && peek().lexeme == "_") { + if (check(TOK_IDENTIFIER) && peek().text(source_) == "_") { advance(); return emit(AstNode{AstTag::PatWildcard, 0, 0, 0, 0, 0}); } @@ -746,7 +906,7 @@ NodeIdx Parser::parseExpression() { if (match(TOK_CONST) || match(TOK_VAR)) { bool isConst = previous().type == TOK_CONST; consume(TOK_IDENTIFIER, "Expected variable name"); - StringIdx name = stringPool->intern(previous().lexeme); + StringIdx name = stringPool->intern(previous().text(source_)); // kNoType signals "infer from init"; astgenVarDecl lowers the // init first in that case and takes its type. Previously this @@ -855,12 +1015,18 @@ NodeIdx Parser::parseExpression() { } if (match(TOK_FOR)) { consume(TOK_IDENTIFIER, "Expected variable name after 'for'"); - StringIdx varName = stringPool->intern(previous().lexeme); + StringIdx varName = stringPool->intern(previous().text(source_)); consume(TOK_IN, "Expected 'in' after for variable"); + // Disable typed struct literals while parsing the range so a + // `for i in 0:n { body }` doesn't greedily swallow `n { body }` + // as a literal. Restored before the body opener. + bool prevAllow = allowStructLit_; + allowStructLit_ = false; NodeIdx start = parseComparison(); consume(TOK_COLON, "Expected ':' in for range"); NodeIdx end = parseComparison(); + allowStructLit_ = prevAllow; consume(TOK_OPEN_BRACE, "Expected '{' after for range"); std::vector body; @@ -1128,7 +1294,7 @@ std::unique_ptr Parser::parseFunction() { if (!isTest) { consume(TOK_FN, "Expected 'fn' keyword"); } consume(TOK_IDENTIFIER, "Expected function name"); - std::string name = previous().lexeme; + std::string name(previous().text(source_)); consume(TOK_OPEN_PAREN, "Expected '(' after function name"); @@ -1145,7 +1311,7 @@ std::unique_ptr Parser::parseFunction() { break; } consume(TOK_IDENTIFIER, "Expected parameter name"); - std::string paramName = previous().lexeme; + std::string paramName(previous().text(source_)); consume(TOK_COLON, "Expected ':' after parameter name"); @@ -1210,7 +1376,7 @@ void Parser::parseStructBody( } // Field: `name: Type`. Comma separates from the next member. consume(TOK_IDENTIFIER, "Expected field name or 'fn'"); - std::string fieldName = previous().lexeme; + std::string fieldName(previous().text(source_)); consume(TOK_COLON, "Expected ':' after field name"); TypeIdx fieldType = parseType(); fields.emplace_back(std::move(fieldName), fieldType); @@ -1224,7 +1390,7 @@ void Parser::parseStructBody( std::unique_ptr Parser::parseStructDecl() { consume(TOK_CONST, "Expected 'const' for struct declaration"); consume(TOK_IDENTIFIER, "Expected struct name"); - std::string name = previous().lexeme; + std::string name(previous().text(source_)); consume(TOK_EQUAL, "Expected '=' after struct name"); consume(TOK_STRUCT, "Expected 'struct' keyword"); consume(TOK_OPEN_BRACE, "Expected '{' after 'struct'"); @@ -1256,7 +1422,7 @@ NodeIdx Parser::parseEnumExpression() { while (!check(TOK_CLOSE_BRACE) && !isAtEnd()) { consume(TOK_IDENTIFIER, "Expected enum variant name"); EnumVariantAST v; - v.Name = previous().lexeme; + v.Name = previous().text(source_); // Optional positional payload list. if (match(TOK_OPEN_PAREN)) { if (!check(TOK_CLOSE_PAREN)) { @@ -1304,7 +1470,7 @@ NodeIdx Parser::parseStructExpression() { std::unique_ptr Parser::parseEnumDecl() { consume(TOK_CONST, "Expected 'const' for enum declaration"); consume(TOK_IDENTIFIER, "Expected enum name"); - std::string name = previous().lexeme; + std::string name(previous().text(source_)); consume(TOK_EQUAL, "Expected '=' after enum name"); consume(TOK_ENUM, "Expected 'enum' keyword"); consume(TOK_OPEN_BRACE, "Expected '{' after 'enum'"); @@ -1314,7 +1480,7 @@ std::unique_ptr Parser::parseEnumDecl() { while (!check(TOK_CLOSE_BRACE) && !isAtEnd()) { consume(TOK_IDENTIFIER, "Expected enum variant name"); EnumVariantAST v; - v.Name = previous().lexeme; + v.Name = previous().text(source_); // Optional payload: `Variant(T1, T2, ...)`. Unit variants omit // the parenthesized list. if (match(TOK_OPEN_PAREN)) { @@ -1332,7 +1498,7 @@ std::unique_ptr Parser::parseEnumDecl() { bool neg = false; uint64_t mag = 0; try { - NumberResult r = parseNumberLiteral(previous().lexeme); + NumberResult r = parseNumberLiteral(previous().text(source_)); if (r.kind != NumberResultKind::Int) { parseError( "Enum discriminant must be a non-negative integer"); @@ -1371,7 +1537,7 @@ std::unique_ptr Parser::parseEnumDecl() { std::unique_ptr Parser::parseUnionDecl() { consume(TOK_CONST, "Expected 'const' for union declaration"); consume(TOK_IDENTIFIER, "Expected union name"); - std::string name = previous().lexeme; + std::string name(previous().text(source_)); consume(TOK_EQUAL, "Expected '=' after union name"); consume(TOK_UNION, "Expected 'union' keyword"); consume(TOK_OPEN_BRACE, "Expected '{' after 'union'"); @@ -1379,7 +1545,7 @@ std::unique_ptr Parser::parseUnionDecl() { std::vector> fields; while (!check(TOK_CLOSE_BRACE) && !isAtEnd()) { consume(TOK_IDENTIFIER, "Expected union field name"); - std::string fieldName = previous().lexeme; + std::string fieldName(previous().text(source_)); consume(TOK_COLON, "Expected ':' after field name"); TypeIdx fieldType = parseType(); fields.emplace_back(std::move(fieldName), fieldType); @@ -1394,12 +1560,15 @@ std::unique_ptr Parser::parseUnionDecl() { std::unique_ptr Parser::parseImportDecl() { consume(TOK_CONST, "Expected 'const' for import declaration"); consume(TOK_IDENTIFIER, "Expected identifier for import name"); - std::string name = previous().lexeme; + std::string name(previous().text(source_)); consume(TOK_EQUAL, "Expected '=' after import name"); consume(TOK_IMPORT, "Expected 'import' keyword"); consume(TOK_OPEN_PAREN, "Expected '(' after 'import'"); consume(TOK_STRING_LITERAL, "Expected string literal for import path"); + // Decoded value (escapes resolved); raw `text(source_)` would + // include the surrounding `"..."` quotes which the resolver + // doesn't expect. std::string path = previous().lexeme; consume(TOK_CLOSE_PAREN, "Expected ')' after import path"); consume(TOK_SEMI, "Expected ';' after import declaration"); @@ -1415,7 +1584,7 @@ std::unique_ptr Parser::parseDestructuringImport() { std::vector names; do { consume(TOK_IDENTIFIER, "Expected identifier in destructuring import"); - names.push_back(previous().lexeme); + names.emplace_back(previous().text(source_)); } while (match(TOK_COMMA)); consume(TOK_CLOSE_BRACE, "Expected '}' after destructuring names"); @@ -1433,7 +1602,7 @@ std::unique_ptr Parser::parseDestructuringImport() { std::unique_ptr Parser::parseConstDecl() { consume(TOK_CONST, "Expected 'const' for module-scope constant"); consume(TOK_IDENTIFIER, "Expected identifier for constant name"); - std::string name = previous().lexeme; + std::string name(previous().text(source_)); TypeIdx declared = kNoType; if (match(TOK_COLON)) { declared = parseType(); } diff --git a/src/parser.h b/src/parser.h index 50f393c..bded5ab 100644 --- a/src/parser.h +++ b/src/parser.h @@ -26,6 +26,13 @@ class ParserAbort {}; class Parser { private: std::vector tokens; + // Reference to the source buffer the tokens index into. Used by + // `Token::text(source_)` whenever the parser needs the actual + // lexeme bytes (identifier names, number literals, etc.). The + // buffer must outlive the Parser — main.cpp pins it in the + // Lexer's owned storage, which survives until the end of the + // compilation. + const std::string &source_; int current = 0; TypePool *typePool; StringPool *stringPool; @@ -44,7 +51,7 @@ class Parser { // Decode a numeric literal lexeme into (magnitude, isNeg, isFloat). // Member rather than free function so it can route over-large / // malformed literals through `parseError` with the token's line. - uint64_t parseNumLexeme(const std::string &s, bool &isNegOut, + uint64_t parseNumLexeme(std::string_view s, bool &isNegOut, bool &isFloatOut) const; NodeIdx parsePrimary(); @@ -74,6 +81,11 @@ class Parser { std::vector> anonEnums; std::vector structContextStack; std::unordered_set importHandles; + // True when an Identifier followed by `{` may be parsed as a typed + // struct literal (`Foo { x: 1 }`). Set false while parsing the + // head of `if`/`while`/`for`/`match` so `if foo { ... }`'s `{` + // stays the then-block opener, not a literal body. + bool allowStructLit_ = true; std::unique_ptr parseUnionDecl(); std::unique_ptr parseEnumDecl(); std::unique_ptr parseConstDecl(); @@ -81,8 +93,8 @@ class Parser { bool isQualifiedNameChain(NodeIdx chainRoot) const; public: - Parser(std::vector tokens, TypePool &typePool, - StringPool &stringPool, NodeStore &nodes, + Parser(std::vector tokens, const std::string &source, + TypePool &typePool, StringPool &stringPool, NodeStore &nodes, jam::Diagnostics *diagnostics = nullptr, std::string filename = ""); std::unique_ptr parse(); std::vector> *sharedAnonStructs = nullptr; diff --git a/src/token.h b/src/token.h index 12de700..9264b5b 100644 --- a/src/token.h +++ b/src/token.h @@ -10,6 +10,7 @@ #include #include +#include // Token types enum TokenType { @@ -83,15 +84,36 @@ enum TokenType { }; // Token structure. +// +// Storage model: `byteOffset` + `length` mark the token's raw span in +// the original source buffer; `text(source)` returns a string_view over +// that span and is the right call for every token kind EXCEPT +// `TOK_STRING_LITERAL`. +// +// `lexeme` is populated only for `TOK_STRING_LITERAL` and carries the +// *decoded* value — escape sequences like `\n` / `\u{2603}` are +// resolved to the bytes the runtime expects, which the raw source +// doesn't contain. Use `lexeme` for string literals, `text(source)` +// for everything else. struct Token { TokenType type; std::string lexeme; int line; uint32_t byteOffset = 0; + uint32_t length = 0; - Token(TokenType type, std::string lexeme, int line, uint32_t byteOffset = 0) + Token(TokenType type, std::string lexeme, int line, uint32_t byteOffset = 0, + uint32_t length = 0) : type(type), lexeme(std::move(lexeme)), line(line), - byteOffset(byteOffset) {} + byteOffset(byteOffset), length(length) {} + + // Raw source span. Caller passes the same `source` the lexer ran + // over. The returned view is valid as long as `source` is. For + // `TOK_STRING_LITERAL` this returns the raw `"..."`-bracketed bytes + // from the source — use `lexeme` for the decoded value. + std::string_view text(const std::string &source) const { + return std::string_view(source).substr(byteOffset, length); + } }; #endif // TOKEN_H diff --git a/tests/cpp/test_analyzer.cpp b/tests/cpp/test_analyzer.cpp new file mode 100644 index 0000000..ebfb755 --- /dev/null +++ b/tests/cpp/test_analyzer.cpp @@ -0,0 +1,313 @@ +// Unit tests for src/analyzer.h — the demand-driven Decl chokepoint. +// Covers the state-machine transitions (Unreferenced → InProgress → +// Complete), the cycle detector (Decl.analysis re-entry produces a +// "dependency loop" diagnostic with the chain), and the struct-status +// chokepoint (Struct.status FieldTypesWIP re-entry produces a +// "struct depends on itself" diagnostic). +// +// Real Decls would carry ast pointers; this suite uses Function / +// Struct decls with null AST pointers — the analyzer treats those +// like "trivial" decls and runs the stub branch. + +#include "../../src/analyzer.h" +#include "../../src/ast.h" +#include "../../src/codegen.h" +#include "../../src/decl.h" +#include "test_framework.h" + +namespace { + +// Codegen context is move-deleted, so each test constructs one in +// place. The analyzer only consumes diagnostics() + currentFile() +// from it for now (Step 2 chokepoint shape); the deeper codegen +// plumbing (type pool, struct registry) becomes a dependency in +// Step 3 when the stub branches get filled. + +void testUnreferencedTransitionsThroughInProgressToComplete() { + JamCodegenContext ctx("test_module"); + ctx.setCurrentFile("test.jam"); + jam::DeclTable decls; + jam::Analyzer az(ctx, decls); + + jam::DeclIndex a = decls.create(jam::DeclKind::Function, "a"); + ASSERT_TRUE(decls.get(a).analysis == jam::DeclAnalysis::Unreferenced); + + jam::DeclValue v = az.ensureDeclAnalyzed(a); + ASSERT_TRUE(decls.get(a).analysis == jam::DeclAnalysis::Complete); + // Function decls get a DeclValue::Function pointing at their AST; + // since we passed no AST it's null but the kind is set. + ASSERT_TRUE(v.kind == jam::DeclValue::Kind::Function); +} + +void testCompleteDeclReturnsCachedValueNoReanalysis() { + JamCodegenContext ctx("test_module"); + ctx.setCurrentFile("test.jam"); + jam::DeclTable decls; + jam::Analyzer az(ctx, decls); + + jam::DeclIndex a = decls.create(jam::DeclKind::Function, "a"); + az.ensureDeclAnalyzed(a); + std::size_t errCountAfterFirst = ctx.diagnostics().errorCount(); + + // Re-ask; expect no new diagnostics + state stays Complete. + jam::DeclValue v = az.ensureDeclAnalyzed(a); + ASSERT_EQ(errCountAfterFirst, ctx.diagnostics().errorCount()); + ASSERT_TRUE(decls.get(a).analysis == jam::DeclAnalysis::Complete); + ASSERT_TRUE(v.kind == jam::DeclValue::Kind::Function); +} + +void testKNoDeclReturnsEmpty() { + JamCodegenContext ctx("test_module"); + ctx.setCurrentFile("test.jam"); + jam::DeclTable decls; + jam::Analyzer az(ctx, decls); + jam::DeclValue v = az.ensureDeclAnalyzed(jam::kNoDecl); + ASSERT_TRUE(v.kind == jam::DeclValue::Kind::None); + ASSERT_FALSE(ctx.diagnostics().hasErrors()); +} + +void testInProgressTriggersCycleError() { + // Simulate re-entering ensureDeclAnalyzed while the decl is mid- + // analysis. We push the decl onto the analysis stack ourselves + // (no public API in Step 2 to force recursion since the stubs are + // non-recursive; Step 3 wires real recursion). + JamCodegenContext ctx("test_module"); + ctx.setCurrentFile("test.jam"); + jam::DeclTable decls; + jam::Analyzer az(ctx, decls); + jam::DeclIndex a = decls.create(jam::DeclKind::Struct, "Cyc"); + + // Manually flip a to InProgress to model the "currently being + // analyzed" scenario from inside one of its dependencies. + decls.get(a).analysis = jam::DeclAnalysis::InProgress; + + std::size_t before = ctx.diagnostics().errorCount(); + jam::DeclValue v = az.ensureDeclAnalyzed(a); + std::size_t after = ctx.diagnostics().errorCount(); + + ASSERT_TRUE(v.kind == jam::DeclValue::Kind::None); + ASSERT_EQ(before + 1, after); + const auto &diag = ctx.diagnostics().all().back(); + ASSERT_TRUE(diag.message.find("dependency loop detected") != + std::string::npos); + ASSERT_TRUE(diag.message.find("Cyc") != std::string::npos); +} + +void testCycleChainListsAnalysisStackInOrder() { + // When the analyzer hits a cycle, the message should list the + // chain in source order: A → B → A. We populate the analysis + // stack ourselves and ask the analyzer to "re-enter" A from the + // middle of B's analysis. + JamCodegenContext ctx("test_module"); + ctx.setCurrentFile("test.jam"); + jam::DeclTable decls; + jam::Analyzer az(ctx, decls); + jam::DeclIndex a = decls.create(jam::DeclKind::Function, "A"); + jam::DeclIndex b = decls.create(jam::DeclKind::Function, "B"); + + decls.get(a).analysis = jam::DeclAnalysis::InProgress; + decls.get(b).analysis = jam::DeclAnalysis::InProgress; + // Stack-driven cycle detection: emulate the recursive call chain + // by writing into the analyzer's analysis stack. We rely on the + // public read-back `analysisStack()` to confirm the read; the + // write is internal so the test pokes the chokepoint by setting + // analysis state directly + relying on pushCycleError using + // whatever the analyzer has on its stack. Step 2 keeps that + // stack empty until ensureDeclAnalyzed runs, so the chain shown + // here is just the repeated decl. Step 3 will exercise the full + // chain through real recursion. + az.ensureDeclAnalyzed(a); + (void)b; + + ASSERT_TRUE(ctx.diagnostics().hasErrors()); + const auto &diag = ctx.diagnostics().all().back(); + ASSERT_TRUE(diag.message.find("`A`") != std::string::npos); +} + +void testResolveTypeFieldsStructNoneToHaveFieldTypes() { + JamCodegenContext ctx("test_module"); + ctx.setCurrentFile("test.jam"); + jam::DeclTable decls; + jam::Analyzer az(ctx, decls); + + jam::DeclIndex s = decls.create(jam::DeclKind::Struct, "S"); + ASSERT_TRUE(decls.get(s).structStatus == jam::StructStatus::None); + + bool ok = az.resolveTypeFieldsStruct(s); + ASSERT_TRUE(ok); + ASSERT_TRUE(decls.get(s).structStatus == jam::StructStatus::HaveFieldTypes); +} + +void testResolveTypeFieldsStructWIPProducesSelfCycleError() { + JamCodegenContext ctx("test_module"); + ctx.setCurrentFile("test.jam"); + jam::DeclTable decls; + jam::Analyzer az(ctx, decls); + + jam::DeclIndex s = decls.create(jam::DeclKind::Struct, "Recursive"); + decls.get(s).structStatus = jam::StructStatus::FieldTypesWIP; + + std::size_t before = ctx.diagnostics().errorCount(); + bool ok = az.resolveTypeFieldsStruct(s); + std::size_t after = ctx.diagnostics().errorCount(); + + ASSERT_FALSE(ok); + ASSERT_EQ(before + 1, after); + const auto &diag = ctx.diagnostics().all().back(); + ASSERT_TRUE(diag.message.find("`Recursive` depends on itself") != + std::string::npos); +} + +void testResolveTypeFieldsStructAlreadyHaveFieldTypesIsNoOp() { + JamCodegenContext ctx("test_module"); + ctx.setCurrentFile("test.jam"); + jam::DeclTable decls; + jam::Analyzer az(ctx, decls); + jam::DeclIndex s = decls.create(jam::DeclKind::Struct, "S"); + decls.get(s).structStatus = jam::StructStatus::HaveFieldTypes; + + std::size_t before = ctx.diagnostics().errorCount(); + bool ok = az.resolveTypeFieldsStruct(s); + ASSERT_TRUE(ok); + ASSERT_EQ(before, ctx.diagnostics().errorCount()); + // State stays put. + ASSERT_TRUE(decls.get(s).structStatus == jam::StructStatus::HaveFieldTypes); +} + +void testResolveTypeFieldsStructOnNonStructDeclIsFalse() { + JamCodegenContext ctx("test_module"); + ctx.setCurrentFile("test.jam"); + jam::DeclTable decls; + jam::Analyzer az(ctx, decls); + jam::DeclIndex f = decls.create(jam::DeclKind::Function, "f"); + bool ok = az.resolveTypeFieldsStruct(f); + ASSERT_FALSE(ok); +} + +void testEnsureDeclAnalyzedFunctionPopulatesSignatureCache() { + // Step C: analyzeFunction caches param + return ABI on the Decl + // so call sites and codegen read one source instead of + // re-classifying. Without a real FunctionAST attached the cache + // stays empty (signature.computed == false) — that's the path + // taken when an analyzer test creates a bare Decl. With an AST, + // the analyzer fills the cache and `computed` flips to true. + JamCodegenContext ctx("test_module"); + ctx.setCurrentFile("test.jam"); + jam::DeclTable decls; + jam::Analyzer az(ctx, decls); + + jam::DeclIndex fn = decls.create(jam::DeclKind::Function, "noFnAst"); + az.ensureDeclAnalyzed(fn); + ASSERT_FALSE(decls.get(fn).signature.computed); + + // Attach a real (empty) FunctionAST and re-analyze a fresh Decl. + FunctionAST realFn("withAst", /*Args=*/{}, + /*ReturnType=*/kNoType, /*Body=*/{}); + jam::DeclIndex fn2 = decls.create(jam::DeclKind::Function, "withAst"); + decls.get(fn2).fnAst = &realFn; + az.ensureDeclAnalyzed(fn2); + ASSERT_TRUE(decls.get(fn2).signature.computed); + // Empty Args → empty params vector; kNoType return → Direct ABI + // classification's default (matches what classifyReturn returns + // for the void-like case). + ASSERT_EQ(static_cast(0), + decls.get(fn2).signature.params.size()); +} + +void testEnsureDeclAnalyzedStructReturnsNamedTypeAndFillsBody() { + // Step A: analyzeStruct(idx) now drives resolveTypeFieldsStruct + // internally and returns a DeclValue::Type whose TypeIdx is the + // Named TypeIdx for the struct's source name. Without a real + // AST attached the body-walk takes the null-AST early-out, but + // the structStatus must still flip to HaveFieldTypes — that's + // the proof that ensureDeclAnalyzed is the single chokepoint. + JamCodegenContext ctx("test_module"); + ctx.setCurrentFile("test.jam"); + jam::DeclTable decls; + jam::Analyzer az(ctx, decls); + + jam::DeclIndex s = decls.create(jam::DeclKind::Struct, "Foo"); + ASSERT_TRUE(decls.get(s).structStatus == jam::StructStatus::None); + + jam::DeclValue v = az.ensureDeclAnalyzed(s); + ASSERT_TRUE(v.kind == jam::DeclValue::Kind::Type); + ASSERT_TRUE(v.type != kNoType); + ASSERT_TRUE(decls.get(s).structStatus == jam::StructStatus::HaveFieldTypes); + ASSERT_TRUE(decls.get(s).analysis == jam::DeclAnalysis::Complete); +} + +void testResolveDeclLooksUpByNameAndAnalyzes() { + // Step F: resolveDecl(name) is the demand-driven name chokepoint. + // It finds the decl by name and forces ensureDeclAnalyzed in one + // call — call sites don't have to know about DeclTable indices. + JamCodegenContext ctx("test_module"); + ctx.setCurrentFile("test.jam"); + jam::DeclTable decls; + jam::Analyzer az(ctx, decls); + + decls.create(jam::DeclKind::Struct, "Foo"); + jam::DeclValue v = az.resolveDecl("Foo"); + ASSERT_TRUE(v.kind == jam::DeclValue::Kind::Type); + ASSERT_TRUE(v.type != kNoType); + + // Unknown name returns a None-kind value with no diagnostic — the + // caller decides whether to error. + std::size_t errBefore = ctx.diagnostics().errorCount(); + jam::DeclValue miss = az.resolveDecl("DoesNotExist"); + ASSERT_TRUE(miss.kind == jam::DeclValue::Kind::None); + ASSERT_EQ(errBefore, ctx.diagnostics().errorCount()); +} + +void testAnalysisStackEmptyAfterTopLevelCall() { + // Each ensureDeclAnalyzed pushes then pops — the stack must be + // empty when control returns to the test driver. + JamCodegenContext ctx("test_module"); + ctx.setCurrentFile("test.jam"); + jam::DeclTable decls; + jam::Analyzer az(ctx, decls); + jam::DeclIndex a = decls.create(jam::DeclKind::Function, "a"); + jam::DeclIndex b = decls.create(jam::DeclKind::Struct, "b"); + az.ensureDeclAnalyzed(a); + az.ensureDeclAnalyzed(b); + ASSERT_EQ(static_cast(0), az.analysisStack().size()); +} + +} // namespace + +int main() { + TestFramework framework; + framework.addTest("Analyzer - Unreferenced → InProgress → Complete", + testUnreferencedTransitionsThroughInProgressToComplete); + framework.addTest("Analyzer - Complete returns cached value", + testCompleteDeclReturnsCachedValueNoReanalysis); + framework.addTest("Analyzer - kNoDecl returns empty", + testKNoDeclReturnsEmpty); + framework.addTest("Analyzer - InProgress triggers cycle error", + testInProgressTriggersCycleError); + framework.addTest("Analyzer - cycle chain lists stack in source order", + testCycleChainListsAnalysisStackInOrder); + framework.addTest( + "Analyzer - resolveTypeFieldsStruct: None → HaveFieldTypes", + testResolveTypeFieldsStructNoneToHaveFieldTypes); + framework.addTest( + "Analyzer - resolveTypeFieldsStruct: WIP is self-cycle error", + testResolveTypeFieldsStructWIPProducesSelfCycleError); + framework.addTest( + "Analyzer - resolveTypeFieldsStruct: HaveFieldTypes is no-op", + testResolveTypeFieldsStructAlreadyHaveFieldTypesIsNoOp); + framework.addTest( + "Analyzer - resolveTypeFieldsStruct on non-struct returns false", + testResolveTypeFieldsStructOnNonStructDeclIsFalse); + framework.addTest("Analyzer - ensureDeclAnalyzed(struct) returns Named " + "TypeIdx + fills body", + testEnsureDeclAnalyzedStructReturnsNamedTypeAndFillsBody); + framework.addTest( + "Analyzer - ensureDeclAnalyzed(fn) populates ABI signature cache", + testEnsureDeclAnalyzedFunctionPopulatesSignatureCache); + framework.addTest("Analyzer - resolveDecl(name) looks up + analyzes", + testResolveDeclLooksUpByNameAndAnalyzes); + framework.addTest("Analyzer - analysis stack empty after top-level call", + testAnalysisStackEmptyAfterTopLevelCall); + framework.runAll(); + return framework.allPassed() ? 0 : 1; +} diff --git a/tests/cpp/test_codegen_errors.cpp b/tests/cpp/test_codegen_errors.cpp index 610d269..94e589a 100644 --- a/tests/cpp/test_codegen_errors.cpp +++ b/tests/cpp/test_codegen_errors.cpp @@ -151,7 +151,7 @@ fn Maybe(T: type) type { storage: T, valid: bool, fn default() Self { - return { storage: T.default(), valid: false }; + return Self { storage: T.default(), valid: false }; } }; } @@ -175,7 +175,7 @@ fn main() i32 { const Bad = struct { n: i32, fn default(self: mut Self) Self { - return { n: 0 }; + return Self { n: 0 }; } }; @@ -291,10 +291,15 @@ fn main() {} // `mod.Private` where Private isn't pub: emits the precise // "is not exported" diagnostic instead of a generic "Unknown". static void testNamespaceNonPubType() { + // The type access itself is what we're testing; using + // `lib.Private` as a fn-param type is enough to fire the + // "is not exported" diagnostic without needing a struct + // literal of that type. auto r = compileWithLib("must_fail_ns_nonpub_type", R"( const lib = import("lib"); -fn main() { var v: lib.Private = { n: 1 }; } +fn takes(p: lib.Private) {} +fn main() {} )", "const Private = struct { n: i32, };\n"); ASSERT_TRUE(r.exitCode != 0); @@ -308,7 +313,8 @@ fn main() { var v: lib.Private = { n: 1 }; } auto r = compileWithLib("must_fail_ns_missing", R"( const lib = import("lib"); -fn main() { var v: lib.Nope = { n: 1 }; } +fn takes(p: lib.Nope) {} +fn main() {} )", "pub const Val = struct { n: i32, };\n"); ASSERT_TRUE(r.exitCode != 0); @@ -319,7 +325,8 @@ fn main() { var v: lib.Nope = { n: 1 }; } // `unknown.X` where `unknown` was never bound to an import. static void testNamespaceUnknownHandle() { auto r = compileSource("must_fail_ns_unknown_handle", R"( -fn main() { var v: unknown.X = { n: 1 }; } +fn takes(p: unknown.X) {} +fn main() {} )"); ASSERT_TRUE(r.exitCode != 0); ASSERT_TRUE(stderrContains(r, "unknown module handle")); @@ -395,7 +402,8 @@ fn main() i32 { return maybe(true); } auto r = compileWithLib("must_fail_bare_name_private", R"( const lib = import("lib"); -fn main() { var v: Private = { n: 1 }; } +fn takes(p: Private) {} +fn main() {} )", "const Private = struct { n: i32, };\n"); ASSERT_TRUE(r.exitCode != 0); diff --git a/tests/cpp/test_decl_table.cpp b/tests/cpp/test_decl_table.cpp new file mode 100644 index 0000000..9af195f --- /dev/null +++ b/tests/cpp/test_decl_table.cpp @@ -0,0 +1,154 @@ +// Unit tests for src/decl.h — DeclTable's identity invariants, the +// symmetric declareDependency edges, and the sentinel slot. The +// Analyzer layer (cycle detection, ensureDeclAnalyzed, struct +// field materialization) gets its own test suite once it lands. + +#include "../../src/decl.h" +#include "test_framework.h" + +namespace { + +void testSentinelSlot() { + jam::DeclTable t; + // Index 0 is the sentinel: findByName for an unknown name returns + // kNoDecl, which IS 0 — callers can treat that as "not found". + ASSERT_EQ(jam::kNoDecl, t.findByName("anything")); + ASSERT_EQ(static_cast(0), t.size()); +} + +void testCreateAssignsMonotonicIndices() { + jam::DeclTable t; + jam::DeclIndex a = t.create(jam::DeclKind::Function, "a"); + jam::DeclIndex b = t.create(jam::DeclKind::Struct, "b"); + jam::DeclIndex c = t.create(jam::DeclKind::Const, "c"); + ASSERT_NE(jam::kNoDecl, a); + ASSERT_NE(jam::kNoDecl, b); + ASSERT_NE(jam::kNoDecl, c); + ASSERT_TRUE(a < b); + ASSERT_TRUE(b < c); + ASSERT_EQ(static_cast(3), t.size()); +} + +void testFindByName() { + jam::DeclTable t; + jam::DeclIndex vec = t.create(jam::DeclKind::Function, "Vec"); + jam::DeclIndex box = t.create(jam::DeclKind::Function, "Box"); + ASSERT_EQ(vec, t.findByName("Vec")); + ASSERT_EQ(box, t.findByName("Box")); + ASSERT_EQ(jam::kNoDecl, t.findByName("Missing")); +} + +void testFindByNameAndKindLandsOnRequestedKind() { + // Jam permits a function and a struct to share a name. The first + // registration wins in `findByName`; `findByNameAndKind` walks + // past it and lands on the kind the caller actually wants. + jam::DeclTable t; + jam::DeclIndex fnCounter = t.create(jam::DeclKind::Function, "Counter"); + jam::DeclIndex structCounter = t.create(jam::DeclKind::Struct, "Counter"); + // findByName returns the first (function). + ASSERT_EQ(fnCounter, t.findByName("Counter")); + // Kind-aware lookup returns each individually. + ASSERT_EQ(fnCounter, + t.findByNameAndKind("Counter", jam::DeclKind::Function)); + ASSERT_EQ(structCounter, + t.findByNameAndKind("Counter", jam::DeclKind::Struct)); + ASSERT_EQ(jam::kNoDecl, + t.findByNameAndKind("Counter", jam::DeclKind::Enum)); + ASSERT_EQ(jam::kNoDecl, + t.findByNameAndKind("Missing", jam::DeclKind::Struct)); +} + +void testDependencyEdgesAreSymmetric() { + jam::DeclTable t; + jam::DeclIndex a = t.create(jam::DeclKind::Function, "a"); + jam::DeclIndex b = t.create(jam::DeclKind::Function, "b"); + jam::DeclIndex c = t.create(jam::DeclKind::Function, "c"); + + // a → b, a → c, b → c + t.declareDependency(a, b); + t.declareDependency(a, c); + t.declareDependency(b, c); + + const auto &ad = t.get(a); + const auto &bd = t.get(b); + const auto &cd = t.get(c); + + // a's dependencies: {b, c}; a is no one's dependant. + ASSERT_EQ(static_cast(2), ad.dependencies.size()); + ASSERT_EQ(static_cast(0), ad.dependants.size()); + // b: one dep (c), one dependant (a). + ASSERT_EQ(static_cast(1), bd.dependencies.size()); + ASSERT_EQ(c, bd.dependencies[0]); + ASSERT_EQ(static_cast(1), bd.dependants.size()); + ASSERT_EQ(a, bd.dependants[0]); + // c: two dependants (a and b), no deps. + ASSERT_EQ(static_cast(0), cd.dependencies.size()); + ASSERT_EQ(static_cast(2), cd.dependants.size()); +} + +void testDuplicateDependencyIsIdempotent() { + jam::DeclTable t; + jam::DeclIndex a = t.create(jam::DeclKind::Function, "a"); + jam::DeclIndex b = t.create(jam::DeclKind::Function, "b"); + t.declareDependency(a, b); + t.declareDependency(a, b); + t.declareDependency(a, b); + ASSERT_EQ(static_cast(1), t.get(a).dependencies.size()); + ASSERT_EQ(static_cast(1), t.get(b).dependants.size()); +} + +void testSelfDependencyIgnored() { + // Decl A depending on itself isn't useful — the cycle detector + // runs on the analysis stack, not the dep graph. The graph + // rejects self-loops up front so trace walks don't infinite-loop. + jam::DeclTable t; + jam::DeclIndex a = t.create(jam::DeclKind::Function, "a"); + t.declareDependency(a, a); + ASSERT_EQ(static_cast(0), t.get(a).dependencies.size()); + ASSERT_EQ(static_cast(0), t.get(a).dependants.size()); +} + +void testKNoDecCallsAreNoOps() { + jam::DeclTable t; + jam::DeclIndex a = t.create(jam::DeclKind::Function, "a"); + // Either side kNoDecl: silently ignored, no crash, no edges added. + t.declareDependency(jam::kNoDecl, a); + t.declareDependency(a, jam::kNoDecl); + ASSERT_EQ(static_cast(0), t.get(a).dependencies.size()); + ASSERT_EQ(static_cast(0), t.get(a).dependants.size()); +} + +void testGetReturnsMutableReference() { + // Verify the mutable accessor lets the analyzer flip analysis state. + // This is exercised by the Analyzer layer; we just sanity-check + // the reference semantics here. + jam::DeclTable t; + jam::DeclIndex a = t.create(jam::DeclKind::Function, "a"); + ASSERT_TRUE(t.get(a).analysis == jam::DeclAnalysis::Unreferenced); + t.get(a).analysis = jam::DeclAnalysis::InProgress; + ASSERT_TRUE(t.get(a).analysis == jam::DeclAnalysis::InProgress); +} + +} // namespace + +int main() { + TestFramework framework; + framework.addTest("DeclTable - sentinel slot present", testSentinelSlot); + framework.addTest("DeclTable - create assigns monotonic indices", + testCreateAssignsMonotonicIndices); + framework.addTest("DeclTable - findByName lookup", testFindByName); + framework.addTest("DeclTable - findByNameAndKind disambiguates", + testFindByNameAndKindLandsOnRequestedKind); + framework.addTest("DeclTable - dependency edges are symmetric", + testDependencyEdgesAreSymmetric); + framework.addTest("DeclTable - duplicate dependency is idempotent", + testDuplicateDependencyIsIdempotent); + framework.addTest("DeclTable - self dependency is ignored", + testSelfDependencyIgnored); + framework.addTest("DeclTable - kNoDecl edges are no-ops", + testKNoDecCallsAreNoOps); + framework.addTest("DeclTable - get() returns mutable reference", + testGetReturnsMutableReference); + framework.runAll(); + return framework.allPassed() ? 0 : 1; +} diff --git a/tests/cpp/test_diagnostics.cpp b/tests/cpp/test_diagnostics.cpp index b856585..6493631 100644 --- a/tests/cpp/test_diagnostics.cpp +++ b/tests/cpp/test_diagnostics.cpp @@ -103,7 +103,7 @@ void testUnknownFieldHasLine() { auto r = compileSource("diag_unknown_field", "const Point = struct { x: i32, y: i32 };\n" "fn main() i32 {\n" - " var p: Point = { x: 1, y: 2 };\n" + " var p: Point = Point { x: 1, y: 2 };\n" " return p.zz;\n" "}\n"); ASSERT_TRUE(r.exitCode != 0); @@ -170,7 +170,7 @@ void testGenericInstantiationCarriesRefTrace() { " };\n" "}\n" "fn main() i32 {\n" - " var b: Box(i32) = { val: 7 };\n" + " var b: Box(i32) = Box(i32) { val: 7 };\n" " return b.pickBad();\n" "}\n"); ASSERT_TRUE(r.exitCode != 0); @@ -236,7 +236,7 @@ void testUnknownMethodIsRecoverable() { "const Point = struct { x: i32 };\n" "fn other() i32 { return 99; }\n" "fn main() i32 {\n" - " var p: Point = { x: 1 };\n" + " var p: Point = Point { x: 1 };\n" " var a: i32 = p.bogusMethod();\n" " var b: i32 = p.alsoBogus();\n" " return a + b;\n" @@ -306,6 +306,181 @@ void testTypeInferenceAllocatesCorrectWidth() { ASSERT_TRUE(r.exitCode == 0); } +void testSelfReferentialStructIsCaught() { + // A struct whose own field is the same struct (without a pointer + // or slice indirection) is infinite-sized. The analyzer's + // `resolveTypeFieldsStruct` flips the struct's status to + // `FieldTypesWIP` before walking fields; re-entering during the + // field walk produces a "struct ... depends on itself" error. + auto r = compileSource("diag_selfref_struct", "const S = struct { x: S };\n" + "fn main() {}\n"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains(r, "`S` depends on itself")); +} + +void testSelfReferentialStructViaPointerIsAllowed() { + // Indirect self-reference (through a pointer) is fine — the + // struct's size doesn't depend on its own size. This shape is + // the standard linked-list / tree node idiom. + auto r = compileSource("diag_selfref_struct_ptr", + "const Node = struct { next: *mut Node };\n" + "fn main() {}\n"); + ASSERT_TRUE(r.exitCode == 0); +} + +void testMultiStepStructCycleShowsReferenceTrace() { + // A two-step cycle: `A { b: B }`, `B { a: A }`. The closing + // error names `A`; the trace shows `B` as the intermediate + // reference. The user reading just the message wouldn't know + // the loop closed via B — the trace is what makes the cycle + // shape legible. + auto r = compileSource("diag_multistep_struct_cycle", + "const A = struct { b: B };\n" + "const B = struct { a: A };\n" + "fn main() {}\n"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains(r, "`A` depends on itself")); + ASSERT_TRUE(stderrContains(r, "referenced by `B`")); +} + +void testSelfReferentialEnumIsCaught() { + // A payloaded enum variant that carries the enum itself is + // infinite-sized — the analyzer's resolveTypeFieldsEnum walks + // payload TypeIdxs and, when it sees a direct enum/struct/union + // payload, triggers ensureEnumBody/Struct/Union which trips the + // matching BodyWIP / FieldTypesWIP cycle key. + auto r = compileSource("diag_selfref_enum", + "const E = enum { Nothing, Just(E) };\n" + "fn main() {}\n"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains(r, "`E` depends on itself")); +} + +void testSelfReferentialUnionIsCaught() { + auto r = compileSource("diag_selfref_union", + "const U = union { a: U, b: i32 };\n" + "fn main() {}\n"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains(r, "`U` depends on itself")); +} + +void testStructViaUnionCycleIsCaught() { + // Mixed struct→union→struct chain. The cycle key here is on the + // inner struct's FieldTypesWIP — the union's body fill triggers + // ensureStructBody, which re-enters S while it's still WIP. + auto r = compileSource("diag_struct_via_union", + "const U = union { s: S, b: i32 };\n" + "const S = struct { u: U };\n" + "fn main() {}\n"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains(r, "depends on itself")); +} + +void testThreeStepStructCycleShowsFullChain() { + // `A { b: B }`, `B { c: C }`, `C { a: A }`. The chain in the + // trace is the full ancestor list — both `B` and `C` show up. + auto r = + compileSource("diag_three_step_cycle", "const A = struct { b: B };\n" + "const B = struct { c: C };\n" + "const C = struct { a: A };\n" + "fn main() {}\n"); + ASSERT_TRUE(r.exitCode != 0); + ASSERT_TRUE(stderrContains(r, "`A` depends on itself")); + ASSERT_TRUE(stderrContains(r, "referenced by `B`")); + ASSERT_TRUE(stderrContains(r, "referenced by `C`")); +} + +void testChainedMethodCallOnLocal() { + // `t.field.method(args)` — multi-segment receiver where the root + // is a local. Pre-fix the parser collapsed the chain into the + // qualified name `t.field.method`, then the codegen looked for a + // function with that name and errored "unknown module handle `t`". + // The fix: parser emits the indirect-call form when the chain + // root isn't a known import handle and the chain has 2+ dots. + // Inner-with-methods would trip the top-level-struct method + // restriction (only drop/default allowed); we use two generic + // wrappers so methods are permitted on both layers. + auto r = compileSource( + "diag_chained_call_local", + "fn Inner(T: type) type {\n" + " return struct {\n" + " val: T,\n" + " fn get(self: Self) T { return self.val; }\n" + " };\n" + "}\n" + "fn Wrap(T: type) type {\n" + " return struct {\n" + " inner: Inner(T),\n" + " fn make(v: T) Self {\n" + " return Self { inner: Inner(T) { val: v } };\n" + " }\n" + " };\n" + "}\n" + "fn main() i32 {\n" + " var w: Wrap(i32) = Wrap(i32).make(7);\n" + " return w.inner.get();\n" + "}\n"); + ASSERT_TRUE(r.exitCode == 0); +} + +void testTwoDotStaticCallStillWorks() { + // `Random.default()` style — single dot, type-static dispatch. + // Must keep working (regression guard for the parser fix). + auto r = compileSource("diag_static_call_one_dot", + "const Random = struct {\n" + " n: i32,\n" + " fn default() Self { return Self { n: 0 }; }\n" + "};\n" + "fn main() i32 {\n" + " var r: Random = Random.default();\n" + " return r.n;\n" + "}\n"); + ASSERT_TRUE(r.exitCode == 0); +} + +void testGenericMethodChainedThroughLocalField() { + // The case from the user's bug report: a struct has a field of + // generic-instantiated type, and an instance method is invoked + // through `local.field.method(args)`. The chain has two dots and + // `local` is in the locals table — must take the indirect-call + // path. + auto r = compileSource( + "diag_generic_via_field", + "const {Vec} = import(\"collections\");\n" + "const Random = struct {\n" + " inner: Vec(i32),\n" + " fn default() Self { return Self { inner: Vec(i32).empty() }; }\n" + "};\n" + "fn main() i32 {\n" + " var t = Random.default();\n" + " t.inner.push(1);\n" + " t.inner.push(2);\n" + " return 0;\n" + "}\n"); + ASSERT_TRUE(r.exitCode == 0); +} + +void testFunctionAndStructMayShareName() { + // `fn Counter(T: type) type` and `const Counter = struct {...}` + // can coexist — the function lives in the function namespace, + // the struct in the type namespace. The analyzer's kind-aware + // finder lands on the right Decl depending on the lookup + // context, so the struct's body still gets materialised even + // when the same-name function was registered first. + auto r = compileSource( + "diag_fn_struct_same_name", + "fn Counter(T: type) type { return struct { value: T }; }\n" + "const Counter = struct {\n" + " n: i32,\n" + " fn default() Self { return Self { n: 0 }; }\n" + "};\n" + "fn main() i32 {\n" + " var c: Counter = Counter.default();\n" + " return c.n;\n" + "}\n"); + ASSERT_TRUE(r.exitCode == 0); +} + void testIntegerOverflowLiteralHasLine() { auto r = compileSource("diag_intover", "fn main() i32 {\n" @@ -390,6 +565,30 @@ class DiagnosticTests { framework.addTest( "Diagnostics - inferred var width correct for bool / float", testTypeInferenceAllocatesCorrectWidth); + framework.addTest("Diagnostics - self-referential struct rejected", + testSelfReferentialStructIsCaught); + framework.addTest( + "Diagnostics - self-referential struct via pointer ok", + testSelfReferentialStructViaPointerIsAllowed); + framework.addTest("Diagnostics - function and struct may share a name", + testFunctionAndStructMayShareName); + framework.addTest( + "Diagnostics - multi-step struct cycle shows ref trace", + testMultiStepStructCycleShowsReferenceTrace); + framework.addTest("Diagnostics - three-step struct cycle full chain", + testThreeStepStructCycleShowsFullChain); + framework.addTest("Diagnostics - self-referential enum rejected", + testSelfReferentialEnumIsCaught); + framework.addTest("Diagnostics - self-referential union rejected", + testSelfReferentialUnionIsCaught); + framework.addTest("Diagnostics - struct-via-union cycle rejected", + testStructViaUnionCycleIsCaught); + framework.addTest("Parser - chained method call on local works", + testChainedMethodCallOnLocal); + framework.addTest("Parser - single-dot static call still works", + testTwoDotStaticCallStillWorks); + framework.addTest("Parser - local.field.method() on generic field", + testGenericMethodChainedThroughLocalField); framework.addTest("Diagnostics - format is file:line: error:", testDiagnosticFormatIsFileLineError); } diff --git a/tests/cpp/test_init_analysis.cpp b/tests/cpp/test_init_analysis.cpp index f2aee6e..188fabf 100644 --- a/tests/cpp/test_init_analysis.cpp +++ b/tests/cpp/test_init_analysis.cpp @@ -43,7 +43,8 @@ AnalyzeResult analyzeSource(const std::string &src) { Lexer lexer(src); auto tokens = lexer.scanTokens(); - Parser parser(tokens, *result.types, *result.strings, *result.nodes); + Parser parser(tokens, lexer.sourceBuffer(), *result.types, *result.strings, + *result.nodes); result.module = parser.parse(); jam::init_analysis::FunctionRegistry registry; @@ -162,7 +163,7 @@ const Pair = struct { a: u32, b: u32 }; fn modifyAndRead(whole: mut Pair, part: u32) u32 { return part; } fn caller() u32 { - var p: Pair = { a: 1, b: 2 }; + var p: Pair = Pair { a: 1, b: 2 }; return modifyAndRead(p, p.a); } )"); @@ -177,7 +178,7 @@ const Pair = struct { a: u32, b: u32 }; fn add(a: u32, b: u32) u32 { return a + b; } fn caller() u32 { - var p: Pair = { a: 10, b: 20 }; + var p: Pair = Pair { a: 10, b: 20 }; return add(p.a, p.b); } )"); @@ -244,7 +245,7 @@ fn consume(f: move File) i32 { } fn caller() i32 { - var f: File = { fd: 7 }; + var f: File = File { fd: 7 }; return consume(f); } )"); @@ -269,7 +270,7 @@ fn read(f: File) i32 { } fn caller() i32 { - var f: File = { fd: 7 }; + var f: File = File { fd: 7 }; return read(f); } )"); @@ -291,7 +292,7 @@ fn close(f: mut File) { } fn caller() i32 { - var f: File = { fd: 7 }; + var f: File = File { fd: 7 }; close(&f); return f.fd; } @@ -313,7 +314,7 @@ fn consume(p: move Plain) u32 { } fn caller() u32 { - var p: Plain = { a: 1, b: 2 }; + var p: Plain = Plain { a: 1, b: 2 }; return consume(p); } )"); diff --git a/tests/unit/test_abi_extern_mixed.jam b/tests/unit/test_abi_extern_mixed.jam index 9baae0d..9c8889e 100644 --- a/tests/unit/test_abi_extern_mixed.jam +++ b/tests/unit/test_abi_extern_mixed.jam @@ -42,7 +42,7 @@ const Stats = struct { fn statsOf(s: *const[] u8) Stats { var len: u64 = strlen(s); - var out: Stats = { a: len, b: len * 2, c: len * 3 }; + var out: Stats = Stats { a: len, b: len * 2, c: len * 3 }; return out; } @@ -70,7 +70,7 @@ fn fillField(b: mut Bag) i32 { } tfn fillFieldOK() { - var b: Bag = { one: 0, two: 0, three: 0, four: 0 }; + var b: Bag = Bag { one: 0, two: 0, three: 0, four: 0 }; var n: i32 = fillField(&b); assert(n, 2); assert(b.one, 104); // 'h' diff --git a/tests/unit/test_abi_large.jam b/tests/unit/test_abi_large.jam index 85dc0d8..aa7ff11 100644 --- a/tests/unit/test_abi_large.jam +++ b/tests/unit/test_abi_large.jam @@ -42,23 +42,23 @@ fn pickField(prefix: u32, b: Big) u64 { } tfn sumBigOK() { - var x: Big = { a: 10, b: 20, c: 30 }; + var x: Big = Big { a: 10, b: 20, c: 30 }; assert(sumBig(x), 60); } tfn sumWideOK() { - var w: Wide = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }; + var w: Wide = Wide { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }; assert(sumWide(w), 21); } tfn consumeBigOK() { - var x: Big = { a: 100, b: 200, c: 300 }; + var x: Big = Big { a: 100, b: 200, c: 300 }; assert(consumeBig(x), 600); // After consumeBig, x is moved-out — the analyzer would reject any // subsequent read of x. We just don't read it. } tfn pickFieldOK() { - var x: Big = { a: 0, b: 42, c: 0 }; + var x: Big = Big { a: 0, b: 42, c: 0 }; assert(pickField(8, x), 50); } diff --git a/tests/unit/test_abi_sret.jam b/tests/unit/test_abi_sret.jam index d8ec095..c3158c2 100644 --- a/tests/unit/test_abi_sret.jam +++ b/tests/unit/test_abi_sret.jam @@ -14,7 +14,7 @@ const Big = struct { }; fn makeBig(seed: u64) Big { - var b: Big = { a: seed, b: seed * 2, c: seed * 3 }; + var b: Big = Big { a: seed, b: seed * 2, c: seed * 3 }; return b; } @@ -27,7 +27,7 @@ fn sumOfMakeBig(seed: u64) u64 { // sret callee that takes a large parameter (P9.5 + P9.6 together). fn passThroughBig(b: Big) Big { - var copy: Big = { a: b.a + 1, b: b.b + 1, c: b.c + 1 }; + var copy: Big = Big { a: b.a + 1, b: b.b + 1, c: b.c + 1 }; return copy; } @@ -44,7 +44,7 @@ tfn sumOfMakeBigOK() { } tfn passThroughBigOK() { - var src: Big = { a: 100, b: 200, c: 300 }; + var src: Big = Big { a: 100, b: 200, c: 300 }; var got: Big = passThroughBig(src); assert(got.a, 101); assert(got.b, 201); diff --git a/tests/unit/test_array.jam b/tests/unit/test_array.jam index 556deb6..8ffe631 100644 --- a/tests/unit/test_array.jam +++ b/tests/unit/test_array.jam @@ -61,13 +61,13 @@ fn loopFillThenReadLast() u8 { } fn structFieldArrayWrite() u8 { - var g: Game = { score: 0, board: [0; 10] }; + var g: Game = Game { score: 0, board: [0; 10] }; g.board[3] = 42; return g.board[3]; } fn structFieldArrayLoopFill() u8 { - var g: Game = { score: 0, board: [0; 10] }; + var g: Game = Game { score: 0, board: [0; 10] }; for i in 0:10 { g.board[i] = i; } @@ -75,7 +75,7 @@ fn structFieldArrayLoopFill() u8 { } fn structFieldArrayMixed() u8 { - var g: Game = { score: 99, board: [0; 10] }; + var g: Game = Game { score: 99, board: [0; 10] }; g.board[0] = g.score; g.board[1] = 50; return g.board[0]; diff --git a/tests/unit/test_drops.jam b/tests/unit/test_drops.jam index 98c4fac..ca33d90 100644 --- a/tests/unit/test_drops.jam +++ b/tests/unit/test_drops.jam @@ -23,7 +23,7 @@ fn drop(self: mut Counter) { fn singleScope() u32 { var hits: u32 = 0; - var c: Counter = { value: 7, sink: &hits }; + var c: Counter = Counter { value: 7, sink: &hits }; // c drops here at function end → hits becomes 1 return c.value + 100; } @@ -36,8 +36,8 @@ fn callerObservingDrop() u32 { } fn wrapsCounter(sink: *mut u32) u32 { - var c: Counter = { value: 5, sink: sink }; - var d: Counter = { value: 11, sink: sink }; + var c: Counter = Counter { value: 5, sink: sink }; + var d: Counter = Counter { value: 11, sink: sink }; // Both c and d drop at function exit (reverse-decl: d first, then c). // Each drop bumps `*sink` by 1 → sink becomes 2. return c.value + d.value; @@ -50,7 +50,7 @@ fn earlyReturnDropsToo(b: bool) u32 { } fn early(b: bool, sink: *mut u32) u32 { - var c: Counter = { value: 13, sink: sink }; + var c: Counter = Counter { value: 13, sink: sink }; if (b) { return c.value; } diff --git a/tests/unit/test_drops_loops.jam b/tests/unit/test_drops_loops.jam index 3255fb6..de7329a 100644 --- a/tests/unit/test_drops_loops.jam +++ b/tests/unit/test_drops_loops.jam @@ -22,7 +22,7 @@ fn breakDropsBody() u32 { fn breakInner(sink: *mut u32) { for i in 0:10 { - var x: Bumper = { sink: sink }; + var x: Bumper = Bumper { sink: sink }; if (i == 3) { // x must drop before the break exits the loop → bumps once break; @@ -40,7 +40,7 @@ fn continueDropsBody() u32 { fn continueInner(sink: *mut u32) { for i in 0:5 { - var x: Bumper = { sink: sink }; + var x: Bumper = Bumper { sink: sink }; if (i == 2) { // x must drop before the continue jumps to next iter continue; @@ -59,9 +59,9 @@ fn breakNestedScopes() u32 { fn breakNestedInner(sink: *mut u32) { for i in 0:10 { - var outer: Bumper = { sink: sink }; + var outer: Bumper = Bumper { sink: sink }; if (i == 1) { - var inner: Bumper = { sink: sink }; + var inner: Bumper = Bumper { sink: sink }; // break should drop: inner first, then outer, then exit // → bumps twice on this path break; diff --git a/tests/unit/test_drops_mangling.jam b/tests/unit/test_drops_mangling.jam index 1bbe4b1..838d160 100644 --- a/tests/unit/test_drops_mangling.jam +++ b/tests/unit/test_drops_mangling.jam @@ -29,7 +29,7 @@ fn dropOneA() u32 { } fn onlyA(sink: *mut u32) u32 { - var a: A = { sink: sink }; + var a: A = A { sink: sink }; return 1; } @@ -40,7 +40,7 @@ fn dropOneB() u32 { } fn onlyB(sink: *mut u32) u32 { - var b: B = { sink: sink }; + var b: B = B { sink: sink }; return 2; } @@ -51,8 +51,8 @@ fn dropBoth() u32 { } fn bothInOne(sink: *mut u32) u32 { - var a: A = { sink: sink }; - var b: B = { sink: sink }; + var a: A = A { sink: sink }; + var b: B = B { sink: sink }; return 3; } diff --git a/tests/unit/test_drops_scoped.jam b/tests/unit/test_drops_scoped.jam index f942cb8..5fa8998 100644 --- a/tests/unit/test_drops_scoped.jam +++ b/tests/unit/test_drops_scoped.jam @@ -17,7 +17,7 @@ fn drop(self: mut Bumper) { fn ifBodyDropsAtEnd(b: bool) u32 { var hits: u32 = 0; if (b) { - var x: Bumper = { sink: &hits }; + var x: Bumper = Bumper { sink: &hits }; // x goes out of scope at end of this block → drop runs → hits=1 } // We can read hits here; if drop ran at end of if, hits == 1. @@ -33,7 +33,7 @@ fn elseBodyDropsAtEnd() u32 { if (false) { var ignored: u32 = 0; } else { - var x: Bumper = { sink: &hits }; + var x: Bumper = Bumper { sink: &hits }; // drop at end of else → hits=1 } return hits; @@ -43,7 +43,7 @@ fn elseBodyDropsAtEnd() u32 { fn loopIterationDrops() u32 { var hits: u32 = 0; for i in 0:5 { - var x: Bumper = { sink: &hits }; + var x: Bumper = Bumper { sink: &hits }; // drop at end of iteration body → hits++ each pass } return hits; @@ -52,9 +52,9 @@ fn loopIterationDrops() u32 { // --- Outer drop fires at function end; inner drop fires at end of if --- fn nested(b: bool) u32 { var hits: u32 = 0; - var outer: Bumper = { sink: &hits }; + var outer: Bumper = Bumper { sink: &hits }; if (b) { - var inner: Bumper = { sink: &hits }; + var inner: Bumper = Bumper { sink: &hits }; // inner drops at end of if → hits=1 } var afterIf: u32 = hits; @@ -65,9 +65,9 @@ fn nested(b: bool) u32 { // --- Return inside an if body: ALL active scopes drop before ret --- fn earlyReturnDropsAll() u32 { var hits: u32 = 0; - var outer: Bumper = { sink: &hits }; + var outer: Bumper = Bumper { sink: &hits }; if (true) { - var inner: Bumper = { sink: &hits }; + var inner: Bumper = Bumper { sink: &hits }; return hits; // never reached } diff --git a/tests/unit/test_generics_basic.jam b/tests/unit/test_generics_basic.jam index 7ab0f2b..ad4549d 100644 --- a/tests/unit/test_generics_basic.jam +++ b/tests/unit/test_generics_basic.jam @@ -24,13 +24,13 @@ fn Box(T: type) type { } tfn boxOfI32() { - var b: Box(i32) = { value: 17 }; + var b: Box(i32) = Box(i32) { value: 17 }; assert(b.value, 17); } tfn boxOfU8DistinctFromBoxOfI32() { - var bi: Box(i32) = { value: 1 }; - var bu: Box(u8) = { value: 2 }; + var bi: Box(i32) = Box(i32) { value: 1 }; + var bu: Box(u8) = Box(u8) { value: 2 }; // Both bindings hold their own struct types with their own field // sizes (4 bytes vs 1 byte). Same field name; different storage. assert(bi.value, 1); @@ -45,13 +45,13 @@ fn Pair(A: type, B: type) type { } tfn twoParamGeneric() { - var p: Pair(i32, u8) = { first: 7, second: 35 }; + var p: Pair(i32, u8) = Pair(i32, u8) { first: 7, second: 35 }; assert(p.first + (p.second as i32), 42); } tfn sameInstantiationReusesType() { - var p1: Pair(i32, u8) = { first: 1, second: 2 }; - var p2: Pair(i32, u8) = { first: 3, second: 4 }; + var p1: Pair(i32, u8) = Pair(i32, u8) { first: 1, second: 2 }; + var p2: Pair(i32, u8) = Pair(i32, u8) { first: 3, second: 4 }; // Both bindings should share the same instantiated struct type // (verified by the compiler accepting a homogeneous-type comparison // through the field accesses below). @@ -65,7 +65,7 @@ tfn sameInstantiationReusesType() { const BoxI32 = Box(i32); tfn typeAliasResolvesToInstantiation() { - var b: BoxI32 = { value: 17 }; + var b: BoxI32 = BoxI32 { value: 17 }; assert(b.value, 17); } @@ -87,7 +87,7 @@ fn Holder(T: type) type { const HolderI32 = Holder(i32); tfn methodOnInstantiatedTypeRunsAtCallSite() { - var h: HolderI32 = { value: 7 }; + var h: HolderI32 = HolderI32 { value: 7 }; assert(HolderI32.unwrap(h), 7); } @@ -112,7 +112,7 @@ fn Counter(T: type) type { const CounterI32 = Counter(i32); fn dropFires(sink: *mut u32) { - var c: CounterI32 = { sink: sink }; + var c: CounterI32 = CounterI32 { sink: sink }; // c drops at function exit, bumping *sink to 1. } @@ -135,7 +135,7 @@ fn Node(T: type) type { const NodeI32 = Node(i32); tfn selfInFieldTypeSubstitutes() { - var head: NodeI32 = { value: 7, next: &head }; + var head: NodeI32 = NodeI32 { value: 7, next: &head }; assert(head.value, 7); } @@ -151,7 +151,7 @@ const PtrToI32 = PtrTo(i32); tfn pointerFieldSubstitutes() { var v: i32 = 42; - var p: PtrToI32 = { ptr: &v }; + var p: PtrToI32 = PtrToI32 { ptr: &v }; var x: *mut i32 = p.ptr; assert(x.*, 42); } @@ -166,7 +166,7 @@ fn ThreeOf(T: type) type { const ThreeI32 = ThreeOf(i32); tfn arrayFieldSubstitutes() { - var t: ThreeI32 = { items: [7, 35, 0] }; + var t: ThreeI32 = ThreeI32 { items: [7, 35, 0] }; assert(t.items[0] + t.items[1], 42); } @@ -180,8 +180,8 @@ fn BoxOuter(T: type) type { const NestedBox = BoxOuter(BoxOuter(i32)); tfn nestedGenericInstantiates() { - var inner: BoxOuter(i32) = { value: 7 }; - var outer: NestedBox = { value: inner }; + var inner: BoxOuter(i32) = BoxOuter(i32) { value: 7 }; + var outer: NestedBox = NestedBox { value: inner }; assert(outer.value.value, 7); } @@ -192,7 +192,7 @@ fn Builder(T: type) type { return struct { value: T, fn make(v: T) Self { - var s: Self = { value: v }; + var s: Self = Self { value: v }; return s; } fn doubled(v: T) Self { @@ -217,7 +217,7 @@ fn Cloner(T: type) type { value: T, fn cloned(self: Self) Self { var v: T = self.value; - var s: Self = { value: v }; + var s: Self = Self { value: v }; return s; } }; @@ -226,7 +226,7 @@ fn Cloner(T: type) type { const ClonerI32 = Cloner(i32); tfn typeParamInMethodBody() { - var orig: ClonerI32 = { value: 42 }; + var orig: ClonerI32 = ClonerI32 { value: 42 }; var c: ClonerI32 = ClonerI32.cloned(orig); assert(c.value, 42); } @@ -238,7 +238,7 @@ fn Wrap(T: type) type { return struct { value: T, fn make(v: T) Self { - var s: Self = { value: v }; + var s: Self = Self { value: v }; return s; } fn unwrap(self: move Self) T { @@ -275,8 +275,8 @@ const BumperI32 = Bumper(i32); const NestedBumper = Bumper(BumperI32); fn doubleDropper(sink: *mut u32) { - var inner: BumperI32 = { sink: sink, value: 7 }; - var outer: NestedBumper = { sink: sink, value: inner }; + var inner: BumperI32 = BumperI32 { sink: sink, value: 7 }; + var outer: NestedBumper = NestedBumper { sink: sink, value: inner }; } tfn nestedDropFiresOnEachLayer() { @@ -293,7 +293,7 @@ tfn nestedDropFiresOnEachLayer() { const Counter = struct { n: i32, fn default() Self { - return { n: 0 }; + return Self { n: 0 }; } }; @@ -311,7 +311,7 @@ fn Pair2(T: type) type { a: T, b: T, fn default() Self { - return { a: T.default(), b: T.default() }; + return Self { a: T.default(), b: T.default() }; } }; } diff --git a/tests/unit/test_indexed_field_chain.jam b/tests/unit/test_indexed_field_chain.jam index d645054..43cc869 100644 --- a/tests/unit/test_indexed_field_chain.jam +++ b/tests/unit/test_indexed_field_chain.jam @@ -20,7 +20,7 @@ const Board = struct { }; tfn writeArrayElemField() { - var arr: [2]Cpu = [{ pc: 0, sp: 0 }; 2]; + var arr: [2]Cpu = [Cpu { pc: 0, sp: 0 }; 2]; arr[0].pc = 42; arr[1].pc = 99; arr[1].sp = 7; @@ -30,7 +30,7 @@ tfn writeArrayElemField() { } tfn writeArrayElemNestedField() { - var arr: [2]Outer = [{ inner: { a: 0, b: 0 }, tag: 0 }; 2]; + var arr: [2]Outer = [Outer { inner: Inner { a: 0, b: 0 }, tag: 0 }; 2]; arr[0].inner.a = 10; arr[0].inner.b = 20; arr[0].tag = 1; @@ -44,7 +44,7 @@ tfn writeArrayElemNestedField() { } tfn writeIndexedFieldThroughStruct() { - var b: Board = { cells: [{ pc: 0, sp: 0 }; 4] }; + var b: Board = Board { cells: [Cpu { pc: 0, sp: 0 }; 4] }; b.cells[0].pc = 100; b.cells[2].sp = 200; b.cells[3].pc = 300; @@ -54,14 +54,14 @@ tfn writeIndexedFieldThroughStruct() { } tfn readArrayElemFieldDirectly() { - var arr: [3]Cpu = [{ pc: 0, sp: 0 }; 3]; + var arr: [3]Cpu = [Cpu { pc: 0, sp: 0 }; 3]; arr[1].pc = 77; var x: u32 = arr[1].pc; assert(x, 77); } tfn computeWithIndexedField() { - var arr: [2]Cpu = [{ pc: 5, sp: 0 }; 2]; + var arr: [2]Cpu = [Cpu { pc: 5, sp: 0 }; 2]; arr[0].pc = 5; arr[1].pc = 10; var sum: u32 = arr[0].pc + arr[1].pc; @@ -69,7 +69,7 @@ tfn computeWithIndexedField() { } tfn indexExpressionWithSideEffectArith() { - var arr: [4]Cpu = [{ pc: 0, sp: 0 }; 4]; + var arr: [4]Cpu = [Cpu { pc: 0, sp: 0 }; 4]; var i: u64 = 0; arr[i + 2].pc = 88; assert(arr[2].pc, 88); @@ -92,7 +92,7 @@ fn CounterFactory(T: type) type { const CounterU32 = CounterFactory(u32); tfn methodCallOnIndexedReceiverMut() { - var arr: [3]CounterU32 = [{ v: 0 }; 3]; + var arr: [3]CounterU32 = [CounterU32 { v: 0 }; 3]; arr[0].bump(10); arr[1].bump(20); arr[2].bump(30); @@ -102,7 +102,7 @@ tfn methodCallOnIndexedReceiverMut() { } tfn methodCallOnIndexedReceiverLet() { - var arr: [2]CounterU32 = [{ v: 5 }; 2]; + var arr: [2]CounterU32 = [CounterU32 { v: 5 }; 2]; arr[0].bump(3); arr[1].bump(7); assert(arr[0].read(), 8); @@ -110,7 +110,7 @@ tfn methodCallOnIndexedReceiverLet() { } tfn methodCallOnIndexedReceiverChained() { - var arr: [2]CounterU32 = [{ v: 1 }; 2]; + var arr: [2]CounterU32 = [CounterU32 { v: 1 }; 2]; arr[0].bump(arr[1].read() + 9); assert(arr[0].read(), 11); } diff --git a/tests/unit/test_init.jam b/tests/unit/test_init.jam index 3029b3b..c6d5a85 100644 --- a/tests/unit/test_init.jam +++ b/tests/unit/test_init.jam @@ -55,7 +55,7 @@ const Vec = struct { // --- Field reassignment after struct-literal init --- fn fieldReassign() u32 { - var v: Vec = { x: 0, y: 0 }; + var v: Vec = Vec { x: 0, y: 0 }; v.x = 7; v.y = 11; return v.x + v.y; diff --git a/tests/unit/test_instance_method_call.jam b/tests/unit/test_instance_method_call.jam index 2771cfa..d3596ca 100644 --- a/tests/unit/test_instance_method_call.jam +++ b/tests/unit/test_instance_method_call.jam @@ -14,7 +14,7 @@ fn Holder(T: type) type { const HolderI32 = Holder(i32); tfn instanceMethod() { - var h: HolderI32 = { value: 7 }; + var h: HolderI32 = HolderI32 { value: 7 }; assert(h.get(), 7); } @@ -36,7 +36,7 @@ fn Holder2(T: type) type { const Holder2I32 = Holder2(i32); tfn selfMethodCallFromBody() { - var h: Holder2I32 = { value: 11 }; + var h: Holder2I32 = Holder2I32 { value: 11 }; assert(h.getTwice(), 11); } @@ -66,14 +66,14 @@ fn Accumulator(T: type) type { const AccumI32 = Accumulator(i32); tfn mutInstanceMethod() { - var a: AccumI32 = { sum: 0 }; + var a: AccumI32 = AccumI32 { sum: 0 }; a.add(3); a.add(5); assert(a.total(), 8); } tfn selfMutMethodFromMutBody() { - var a: AccumI32 = { sum: 0 }; + var a: AccumI32 = AccumI32 { sum: 0 }; a.addTwice(7); assert(a.total(), 14); } diff --git a/tests/unit/test_match_scrutinee_shapes.jam b/tests/unit/test_match_scrutinee_shapes.jam index 1c466a7..395556d 100644 --- a/tests/unit/test_match_scrutinee_shapes.jam +++ b/tests/unit/test_match_scrutinee_shapes.jam @@ -21,7 +21,7 @@ fn scoreOfFieldMatch(g: Game) u32 { } tfn matchOnStructField() { - var g: Game = { color: Color.Red, score: 0 }; + var g: Game = Game { color: Color.Red, score: 0 }; assert(scoreOfFieldMatch(g), 1); g.color = Color.Green; diff --git a/tests/unit/test_modes_escape.jam b/tests/unit/test_modes_escape.jam index 0b875e8..63dda79 100644 --- a/tests/unit/test_modes_escape.jam +++ b/tests/unit/test_modes_escape.jam @@ -45,7 +45,7 @@ tfn doubleByValueOK() { } tfn pickFieldAOK() { - var p: Pair = { a: 9, b: 0 }; + var p: Pair = Pair { a: 9, b: 0 }; assert(pickFieldA(&p), 10); } diff --git a/tests/unit/test_modes_exclusivity.jam b/tests/unit/test_modes_exclusivity.jam index 4bd5e0f..543f0a1 100644 --- a/tests/unit/test_modes_exclusivity.jam +++ b/tests/unit/test_modes_exclusivity.jam @@ -31,7 +31,7 @@ fn addPair(a: u32, b: u32) u32 { } fn disjointFields() u32 { - var p: Pair = { a: 10, b: 20 }; + var p: Pair = Pair { a: 10, b: 20 }; return addPair(p.a, p.b); } diff --git a/tests/unit/test_pointer.jam b/tests/unit/test_pointer.jam index 91b25b4..141cd90 100644 --- a/tests/unit/test_pointer.jam +++ b/tests/unit/test_pointer.jam @@ -41,7 +41,7 @@ const Point = struct { }; fn addressOfStructField() u8 { - var pt: Point = { x: 10, y: 20 }; + var pt: Point = Point { x: 10, y: 20 }; var px: *mut u8 = &pt.x; px.* = 200; return pt.x; diff --git a/tests/unit/test_struct.jam b/tests/unit/test_struct.jam index 580160b..2bbdc81 100644 --- a/tests/unit/test_struct.jam +++ b/tests/unit/test_struct.jam @@ -6,7 +6,7 @@ const Inner = struct { a: u8, b: u8 }; const Outer = struct { inner: Inner, c: u8 }; tfn vec3_with_float_fields_compiles_and_runs() { - const v: Vec3 = { + const v: Vec3 = Vec3 { x: 0, y: 100, z: 50, @@ -14,21 +14,21 @@ tfn vec3_with_float_fields_compiles_and_runs() { } tfn pixel_field_access_returns_correct_values() { - const px: Pixel = { r: 10, g: 20, b: 30 }; + const px: Pixel = Pixel { r: 10, g: 20, b: 30 }; assert(px.r, 10); assert(px.g, 20); assert(px.b, 30); } tfn pixel_fields_compose_in_arithmetic() { - const px: Pixel = { r: 5, g: 10, b: 15 }; + const px: Pixel = Pixel { r: 5, g: 10, b: 15 }; assert(px.r + px.g, 15); assert(px.r + px.b, 20); } tfn multiple_pixel_instances_are_independent() { - const a: Pixel = { r: 1, g: 2, b: 3 }; - const b: Pixel = { r: 100, g: 200, b: 250 }; + const a: Pixel = Pixel { r: 1, g: 2, b: 3 }; + const b: Pixel = Pixel { r: 100, g: 200, b: 250 }; assert(a.r, 1); assert(b.r, 100); assert(a.g, 2); @@ -36,14 +36,14 @@ tfn multiple_pixel_instances_are_independent() { } tfn player_holds_mixed_field_types() { - const p: Player = { hp: 1000000, level: 42, alive: true }; + const p: Player = Player { hp: 1000000, level: 42, alive: true }; assert(p.hp, 1000000); assert(p.level, 42); } tfn nested_struct_literal_works() { - const x: Outer = { - inner: { a: 1, b: 2 }, + const x: Outer = Outer { + inner: Inner { a: 1, b: 2 }, c: 3, }; assert(x.inner.a, 1); @@ -52,7 +52,7 @@ tfn nested_struct_literal_works() { } tfn field_assignment_modifies_struct() { - var px: Pixel = { r: 0, g: 0, b: 0 }; + var px: Pixel = Pixel { r: 0, g: 0, b: 0 }; px.r = 100; px.g = 200; px.b = 50; @@ -62,7 +62,7 @@ tfn field_assignment_modifies_struct() { } tfn nested_field_assignment_modifies_struct() { - var x: Outer = { inner: { a: 0, b: 0 }, c: 0 }; + var x: Outer = Outer { inner: Inner { a: 0, b: 0 }, c: 0 }; x.inner.a = 7; x.inner.b = 8; x.c = 9; @@ -78,7 +78,7 @@ tfn variable_assignment_modifies_value() { } fn make_pixel(r_val: u8, g_val: u8, b_val: u8) Pixel { - const p: Pixel = { r: r_val, g: g_val, b: b_val }; + const p: Pixel = Pixel { r: r_val, g: g_val, b: b_val }; return p; } @@ -94,6 +94,6 @@ fn pixel_red_channel(p: Pixel) u8 { } tfn struct_param_field_access_works() { - const px: Pixel = { r: 77, g: 0, b: 0 }; + const px: Pixel = Pixel { r: 77, g: 0, b: 0 }; assert(pixel_red_channel(px), 77); } diff --git a/tests/unit/test_struct_field_index.jam b/tests/unit/test_struct_field_index.jam index fbe90e4..6fece0e 100644 --- a/tests/unit/test_struct_field_index.jam +++ b/tests/unit/test_struct_field_index.jam @@ -12,7 +12,7 @@ const Box = struct { tfn writeThroughField() { var raw: *mut[] u8 = malloc(64); - var b: Box = { ptr: raw as *mut[] i32 }; + var b: Box = Box { ptr: raw as *mut[] i32 }; b.ptr[0] = 42; b.ptr[1] = 99; b.ptr[2] = 7; @@ -27,7 +27,7 @@ tfn readThroughField() { var p: *mut[] i32 = raw as *mut[] i32; p[0] = 11; p[1] = 22; - var b: Box = { ptr: p }; + var b: Box = Box { ptr: p }; assert(b.ptr[0], 11); assert(b.ptr[1], 22); free(b.ptr as *mut[] u8); @@ -50,7 +50,7 @@ fn Cell(T: type) type { tfn writeThroughSelfField() { var raw: *mut[] u8 = malloc(64); - var c: Cell(i32) = { ptr: raw as *mut[] i32 }; + var c: Cell(i32) = Cell(i32) { ptr: raw as *mut[] i32 }; c.set(0, 5); c.set(1, 17); assert(c.get(0), 5); diff --git a/tests/unit/test_struct_methods.jam b/tests/unit/test_struct_methods.jam index 12c7501..ac44f37 100644 --- a/tests/unit/test_struct_methods.jam +++ b/tests/unit/test_struct_methods.jam @@ -17,7 +17,7 @@ const Counter = struct { // Auto-drop on a binding of a type whose drop is defined inline. fn singleScopeWithInlineDrop() u32 { var hits: u32 = 0; - var c: Counter = { value: 17, sink: &hits }; + var c: Counter = Counter { value: 17, sink: &hits }; // c drops at function end → hits becomes 1. return c.value + hits; } @@ -34,7 +34,7 @@ tfn inlineDropFiresAtScopeExit() { // Caller observes the drop via the sink. Same shape as // callerObservingDrop in test_drops.jam, just with the in-struct form. fn wrapsCounterInline(sink: *mut u32) u32 { - var c: Counter = { value: 5, sink: sink }; + var c: Counter = Counter { value: 5, sink: sink }; return c.value; } @@ -52,7 +52,7 @@ tfn callerObservesInlineDrop() { // docs/STRUCT_METHODS.md as a v1.1 polish item. The test pins down // today's behavior so a future fix is intentional, not accidental. fn callsDropManually(sink: *mut u32) u32 { - var c: Counter = { value: 9, sink: sink }; + var c: Counter = Counter { value: 9, sink: sink }; Counter.drop(&c); // sink == 1 here (manual drop ran) return c.value; diff --git a/tests/unit/test_typecall_method.jam b/tests/unit/test_typecall_method.jam index b27ca40..e3e6c6e 100644 --- a/tests/unit/test_typecall_method.jam +++ b/tests/unit/test_typecall_method.jam @@ -8,7 +8,7 @@ fn Box(T: type) type { return struct { value: T, fn make(v: T) Self { - return { value: v }; + return Self { value: v }; } }; } diff --git a/tests/unit/test_union.jam b/tests/unit/test_union.jam index 5dd5a78..f8434b5 100644 --- a/tests/unit/test_union.jam +++ b/tests/unit/test_union.jam @@ -9,19 +9,19 @@ const FloatBits = union { }; fn writeIntReadInt() u32 { - var b: FloatBits = { i: 0xCAFEBABE }; + var b: FloatBits = FloatBits { i: 0xCAFEBABE }; return b.i; } fn writeIntReadIntZero() u32 { - var b: FloatBits = { i: 0 }; + var b: FloatBits = FloatBits { i: 0 }; return b.i; } fn aliasMostRecentWrite() u32 { // Writing one field then another then reading the second: // returns the second-written value's bit pattern. - var b: FloatBits = { i: 0xAAAAAAAA }; + var b: FloatBits = FloatBits { i: 0xAAAAAAAA }; b.i = 0x55555555; return b.i; } @@ -30,8 +30,8 @@ fn floatRoundTrip(seed: u32) u32 { // Write the bit pattern via u32, read it via f32, then write that // f32 back via the float field, then read via u32 again. The // pattern survives unchanged for any non-NaN, non-signaling value. - var b: FloatBits = { i: seed }; - var b2: FloatBits = { f: b.f }; + var b: FloatBits = FloatBits { i: seed }; + var b2: FloatBits = FloatBits { f: b.f }; return b2.i; } @@ -51,7 +51,7 @@ const SmallVsBig = union { fn writeBigReadSmall() u8 { // u64 written, u8 read — returns the low byte (little-endian on // every target Jam currently supports). - var u: SmallVsBig = { b: 0xDEADBEEFCAFEBABE }; + var u: SmallVsBig = SmallVsBig { b: 0xDEADBEEFCAFEBABE }; return u.s; } @@ -60,7 +60,7 @@ fn writeSmallReadBig() u64 { // alloca held. We first zero via the wide field, then write the // small field, then read the wide field — only the low byte is // defined. - var u: SmallVsBig = { b: 0 }; + var u: SmallVsBig = SmallVsBig { b: 0 }; u.s = 42; return u.b; } @@ -69,7 +69,7 @@ tfn writeBigReadSmallByte() { assert(writeBigReadSmall(), 0xBE); } tfn writeSmallReadBigVal() { assert(writeSmallReadBig(), 42); } fn returnUnion(seed: u32) FloatBits { - var b: FloatBits = { i: seed }; + var b: FloatBits = FloatBits { i: seed }; return b; } @@ -89,7 +89,7 @@ const ThreeWayU32 = union { }; fn signedUnsignedReinterpret() u32 { - var u: ThreeWayU32 = { b: -1 }; // 0xFFFFFFFF as i32 + var u: ThreeWayU32 = ThreeWayU32 { b: -1 }; // 0xFFFFFFFF as i32 return u.a; // read as u32 → 0xFFFFFFFF } @@ -104,12 +104,12 @@ const Bigger = union { }; fn writeBiggestReadSmallest() u8 { - var u: Bigger = { big: 0x0102030405060708 }; + var u: Bigger = Bigger { big: 0x0102030405060708 }; return u.small; // 0x08 on little-endian } fn writeBiggestReadMedium() u32 { - var u: Bigger = { big: 0x0102030405060708 }; + var u: Bigger = Bigger { big: 0x0102030405060708 }; return u.medium; // 0x05060708 on little-endian } -- 2.51.2